popn: fix LED handling (#894)
## Link to GitHub Issue or related Pull Request, if one exists #0 ## Description of change Fix the issue with popn lights flickering. `SetTapeLedDataPart()` updates BI3A’s internal LED buffer without immediately sending it to hardware. High Cheers briefly clears this buffer before writing the complete set of lights. Stage pin-0 (button light) updates in the hook and publish only the latest frame during `GetDeviceStatus()`, matching real BI3A behavior. This prevents temporary clear frames from reaching HID controllers. ## Testing Tested with my iidx controller with HID lights.
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#if SPICE64 && !SPICE_XP
|
#if SPICE64 && !SPICE_XP
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
@@ -141,10 +143,100 @@ namespace games::popn {
|
|||||||
static AIO_SCI_COMM *aioSciComm;
|
static AIO_SCI_COMM *aioSciComm;
|
||||||
static AIO_IOB5_BI3A *aioIob5Bi3a;
|
static AIO_IOB5_BI3A *aioIob5Bi3a;
|
||||||
|
|
||||||
|
static constexpr size_t PIKA_BUTTON_COUNT = 9;
|
||||||
|
static constexpr size_t PIKA_BUTTON_DATA_SIZE = PIKA_BUTTON_COUNT * 3;
|
||||||
|
static std::array<uint8_t, PIKA_BUTTON_DATA_SIZE> pika_button_staging{};
|
||||||
|
static bool pika_button_staging_dirty = false;
|
||||||
|
static std::mutex pika_button_staging_mutex;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* implementations
|
* implementations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
static void publish_pika_button_staging() {
|
||||||
|
std::array<uint8_t, PIKA_BUTTON_DATA_SIZE> data;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
|
||||||
|
if (!pika_button_staging_dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data = pika_button_staging;
|
||||||
|
pika_button_staging_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Lights::popn_lights_t legacy_button_lights[] = {
|
||||||
|
Lights::popn_lights_t::Button1,
|
||||||
|
Lights::popn_lights_t::Button2,
|
||||||
|
Lights::popn_lights_t::Button3,
|
||||||
|
Lights::popn_lights_t::Button4,
|
||||||
|
Lights::popn_lights_t::Button5,
|
||||||
|
Lights::popn_lights_t::Button6,
|
||||||
|
Lights::popn_lights_t::Button7,
|
||||||
|
Lights::popn_lights_t::Button8,
|
||||||
|
Lights::popn_lights_t::Button9
|
||||||
|
};
|
||||||
|
constexpr Lights::popn_lights_t button_lights[] = {
|
||||||
|
Lights::popn_lights_t::PikaButton1_R,
|
||||||
|
Lights::popn_lights_t::PikaButton1_G,
|
||||||
|
Lights::popn_lights_t::PikaButton1_B,
|
||||||
|
Lights::popn_lights_t::PikaButton2_R,
|
||||||
|
Lights::popn_lights_t::PikaButton2_G,
|
||||||
|
Lights::popn_lights_t::PikaButton2_B,
|
||||||
|
Lights::popn_lights_t::PikaButton3_R,
|
||||||
|
Lights::popn_lights_t::PikaButton3_G,
|
||||||
|
Lights::popn_lights_t::PikaButton3_B,
|
||||||
|
Lights::popn_lights_t::PikaButton4_R,
|
||||||
|
Lights::popn_lights_t::PikaButton4_G,
|
||||||
|
Lights::popn_lights_t::PikaButton4_B,
|
||||||
|
Lights::popn_lights_t::PikaButton5_R,
|
||||||
|
Lights::popn_lights_t::PikaButton5_G,
|
||||||
|
Lights::popn_lights_t::PikaButton5_B,
|
||||||
|
Lights::popn_lights_t::PikaButton6_R,
|
||||||
|
Lights::popn_lights_t::PikaButton6_G,
|
||||||
|
Lights::popn_lights_t::PikaButton6_B,
|
||||||
|
Lights::popn_lights_t::PikaButton7_R,
|
||||||
|
Lights::popn_lights_t::PikaButton7_G,
|
||||||
|
Lights::popn_lights_t::PikaButton7_B,
|
||||||
|
Lights::popn_lights_t::PikaButton8_R,
|
||||||
|
Lights::popn_lights_t::PikaButton8_G,
|
||||||
|
Lights::popn_lights_t::PikaButton8_B,
|
||||||
|
Lights::popn_lights_t::PikaButton9_R,
|
||||||
|
Lights::popn_lights_t::PikaButton9_G,
|
||||||
|
Lights::popn_lights_t::PikaButton9_B
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(std::size(legacy_button_lights) == PIKA_BUTTON_COUNT);
|
||||||
|
static_assert(std::size(button_lights) == PIKA_BUTTON_DATA_SIZE);
|
||||||
|
|
||||||
|
auto &lights = get_lights();
|
||||||
|
for (size_t button = 0; button < PIKA_BUTTON_COUNT; button++) {
|
||||||
|
const auto data_offset = button * 3;
|
||||||
|
// special handling converting RGB data to non-RGB lights
|
||||||
|
// take the max(R, G, B) and use it to write the light value
|
||||||
|
const auto light_value = std::max({
|
||||||
|
data[data_offset],
|
||||||
|
data[data_offset + 1],
|
||||||
|
data[data_offset + 2]
|
||||||
|
});
|
||||||
|
|
||||||
|
// on the new cab, buttons are colorless plastic and rely on RGB to be lit
|
||||||
|
// at all times, even when "off". When translating to legacy on/off lights,
|
||||||
|
// treat only values above about 60% brightness as fully on to preserve the
|
||||||
|
// dim "off" state.
|
||||||
|
GameAPI::Lights::writeLight(
|
||||||
|
RI_MGR,
|
||||||
|
lights.at(legacy_button_lights[button]),
|
||||||
|
light_value > 150 ? 1.f : 0.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t light = 0; light < data.size(); light++) {
|
||||||
|
GameAPI::Lights::writeLight(
|
||||||
|
RI_MGR,
|
||||||
|
lights.at(button_lights[light]),
|
||||||
|
data[light] / 255.f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// libaio.dll
|
// libaio.dll
|
||||||
|
|
||||||
static AIO_SCI_COMM_T *__fastcall aioSciComm_Open(AIO_SCI_COMM_T *unk) {
|
static AIO_SCI_COMM_T *__fastcall aioSciComm_Open(AIO_SCI_COMM_T *unk) {
|
||||||
@@ -260,6 +352,11 @@ namespace games::popn {
|
|||||||
log_info("bi3a_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p));
|
log_info("bi3a_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p));
|
||||||
|
|
||||||
if (i_pNodeMgr == aioNmgrIob5) {
|
if (i_pNodeMgr == aioNmgrIob5) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
|
||||||
|
pika_button_staging.fill(0);
|
||||||
|
pika_button_staging_dirty = false;
|
||||||
|
}
|
||||||
aioIob5Bi3a = new AIO_IOB5_BI3A;
|
aioIob5Bi3a = new AIO_IOB5_BI3A;
|
||||||
log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a));
|
log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a));
|
||||||
return aioIob5Bi3a;
|
return aioIob5Bi3a;
|
||||||
@@ -269,11 +366,15 @@ namespace games::popn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void __fastcall aioIob5Bi3a_GetDeviceStatus(AIO_IOB5_BI3A *i_pNodeCtl, AIO_IOB5_BI3A__DEVSTATUS *o_DevStatus) {
|
static void __fastcall aioIob5Bi3a_GetDeviceStatus(AIO_IOB5_BI3A *i_pNodeCtl, AIO_IOB5_BI3A__DEVSTATUS *o_DevStatus) {
|
||||||
RI_MGR->devices_flush_output();
|
|
||||||
if (i_pNodeCtl != aioIob5Bi3a) {
|
if (i_pNodeCtl != aioIob5Bi3a) {
|
||||||
return aioIob5Bi3a_GetDeviceStatus_orig(i_pNodeCtl, o_DevStatus);
|
return aioIob5Bi3a_GetDeviceStatus_orig(i_pNodeCtl, o_DevStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// snapshot the current state of lights and flush the output
|
||||||
|
// this closely mirrors what the game does with real I/O
|
||||||
|
publish_pika_button_staging();
|
||||||
|
RI_MGR->devices_flush_output();
|
||||||
|
|
||||||
memset(o_DevStatus, 0, sizeof(*o_DevStatus));
|
memset(o_DevStatus, 0, sizeof(*o_DevStatus));
|
||||||
auto &buttons = get_buttons();
|
auto &buttons = get_buttons();
|
||||||
|
|
||||||
@@ -413,61 +514,15 @@ namespace games::popn {
|
|||||||
i_CnPin, number_of_leds, (uint8_t)i_LedType, fmt::ptr(i_pData), data);
|
i_CnPin, number_of_leds, (uint8_t)i_LedType, fmt::ptr(i_pData), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &lights = get_lights();
|
|
||||||
if (i_CnPin == 0 && number_of_leds == 9 * 3) {
|
if (i_CnPin == 0 && number_of_leds == 9 * 3) {
|
||||||
// special handling converting to non-RGB lights
|
// SetTapeLedDataPart is meant to be a temporary staging buffer, so the game occasionally
|
||||||
// take the max(R, G, B) and use it to write the light value
|
// clears the buffer (set to 0 / lights off) between animations but they are not meant
|
||||||
uint8_t light_value[9] = { 0 };
|
// to be seen by the I/O
|
||||||
for (uint32_t i = 0; i < number_of_leds; i += 3) {
|
// therefore, instead of writing the lights out directly here, we stage them in a buffer
|
||||||
light_value[i / 3] =
|
// and wait for GetDeviceStatus to be called
|
||||||
std::max({ ((uint8_t *)i_pData)[i], ((uint8_t *)i_pData)[i + 1], ((uint8_t *)i_pData)[i + 2] });
|
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
|
||||||
}
|
memcpy(pika_button_staging.data(), i_pData, pika_button_staging.size());
|
||||||
|
pika_button_staging_dirty = true;
|
||||||
constexpr Lights::popn_lights_t legacy_button_lights[] = {
|
|
||||||
Lights::popn_lights_t::Button1,
|
|
||||||
Lights::popn_lights_t::Button2,
|
|
||||||
Lights::popn_lights_t::Button3,
|
|
||||||
Lights::popn_lights_t::Button4,
|
|
||||||
Lights::popn_lights_t::Button5,
|
|
||||||
Lights::popn_lights_t::Button6,
|
|
||||||
Lights::popn_lights_t::Button7,
|
|
||||||
Lights::popn_lights_t::Button8,
|
|
||||||
Lights::popn_lights_t::Button9
|
|
||||||
};
|
|
||||||
|
|
||||||
static_assert(std::size(legacy_button_lights) == 9);
|
|
||||||
|
|
||||||
for (size_t light = 0; light < 9; light++) {
|
|
||||||
// on the new cab, buttons are colorless plastic and they rely on RGB to be lit at all times, even when "off"
|
|
||||||
// when translating to legacy on/off lights, treat anything above ~60% brightness as fully on,
|
|
||||||
// otherwise off, to avoid dimly lit "off" state
|
|
||||||
GameAPI::Lights::writeLight(
|
|
||||||
RI_MGR,
|
|
||||||
lights.at(legacy_button_lights[light]),
|
|
||||||
light_value[light] > 150 ? 1.f : 0.f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// color buttons
|
|
||||||
constexpr Lights::popn_lights_t button_lights[] = {
|
|
||||||
Lights::popn_lights_t::PikaButton1_R, Lights::popn_lights_t::PikaButton1_G, Lights::popn_lights_t::PikaButton1_B,
|
|
||||||
Lights::popn_lights_t::PikaButton2_R, Lights::popn_lights_t::PikaButton2_G, Lights::popn_lights_t::PikaButton2_B,
|
|
||||||
Lights::popn_lights_t::PikaButton3_R, Lights::popn_lights_t::PikaButton3_G, Lights::popn_lights_t::PikaButton3_B,
|
|
||||||
Lights::popn_lights_t::PikaButton4_R, Lights::popn_lights_t::PikaButton4_G, Lights::popn_lights_t::PikaButton4_B,
|
|
||||||
Lights::popn_lights_t::PikaButton5_R, Lights::popn_lights_t::PikaButton5_G, Lights::popn_lights_t::PikaButton5_B,
|
|
||||||
Lights::popn_lights_t::PikaButton6_R, Lights::popn_lights_t::PikaButton6_G, Lights::popn_lights_t::PikaButton6_B,
|
|
||||||
Lights::popn_lights_t::PikaButton7_R, Lights::popn_lights_t::PikaButton7_G, Lights::popn_lights_t::PikaButton7_B,
|
|
||||||
Lights::popn_lights_t::PikaButton8_R, Lights::popn_lights_t::PikaButton8_G, Lights::popn_lights_t::PikaButton8_B,
|
|
||||||
Lights::popn_lights_t::PikaButton9_R, Lights::popn_lights_t::PikaButton9_G, Lights::popn_lights_t::PikaButton9_B
|
|
||||||
};
|
|
||||||
|
|
||||||
static_assert(std::size(button_lights) == 9 * 3);
|
|
||||||
|
|
||||||
for (size_t light = 0; light < number_of_leds; light++) {
|
|
||||||
GameAPI::Lights::writeLight(
|
|
||||||
RI_MGR,
|
|
||||||
lights.at(button_lights[light]),
|
|
||||||
((uint8_t *)i_pData)[light] / 255.f);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) {
|
if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user