From 2628c078fa16aa657ef0f988a6c5922019ad1102 Mon Sep 17 00:00:00 2001 From: Glenn Maynard Date: Thu, 8 Nov 2018 15:37:46 -0600 Subject: [PATCH] Add SMXPanelAnimation. --- sdk/SMX.h | 14 +- sdk/Windows/SMX.cpp | 4 + sdk/Windows/SMX.vcxproj | 2 + sdk/Windows/SMX.vcxproj.filters | 6 + sdk/Windows/SMXPanelAnimation.cpp | 416 ++++++++++++++++++++++++++++++ sdk/Windows/SMXPanelAnimation.h | 50 ++++ smx-config/SMX.cs | 66 ++++- 7 files changed, 552 insertions(+), 6 deletions(-) create mode 100644 sdk/Windows/SMXPanelAnimation.cpp create mode 100644 sdk/Windows/SMXPanelAnimation.h diff --git a/sdk/SMX.h b/sdk/SMX.h index 7ce3898..574d2ec 100644 --- a/sdk/SMX.h +++ b/sdk/SMX.h @@ -135,6 +135,13 @@ enum SMXUpdateCallbackReason { SMXUpdateCallback_FactoryResetCommandComplete }; +// Bits for SMXConfig::flags. +enum SMXConfigFlags { + // This is used to store whether SMXConfig is in GIF animation mode or + // not. If set, SMXConfig will use animations. + PlatformFlags_AutoLightingUsePressedAnimations = 1 << 0, +}; + // The configuration for a connected controller. This can be retrieved with SMX_GetConfig // and modified with SMX_SetConfig. // @@ -227,12 +234,15 @@ struct SMXConfig // This is disabled by default. uint16_t debounceDelayMs = 0; + // Packed flags (currently only used by SMXConfig). + uint8_t flags = 0; + // Pad the struct to 250 bytes. This keeps this struct size from changing // as we add fields, so the ABI doesn't change. Applications should leave // any data in here unchanged when calling SMX_SetConfig. - uint8_t padding[164]; + uint8_t padding[163]; }; -static_assert(offsetof(SMXConfig, padding) == 86, "Expected 86 bytes"); // includes one padding byte +static_assert(offsetof(SMXConfig, padding) == 87, "Expected 87 bytes"); // includes one padding byte static_assert(sizeof(SMXConfig) == 250, "Expected 250 bytes"); // The values (except for Off) correspond with the protocol and must not be changed. diff --git a/sdk/Windows/SMX.cpp b/sdk/Windows/SMX.cpp index 9a14ea9..83a065d 100644 --- a/sdk/Windows/SMX.cpp +++ b/sdk/Windows/SMX.cpp @@ -7,6 +7,7 @@ #include "SMXManager.h" #include "SMXDevice.h" #include "SMXBuildVersion.h" +#include "SMXPanelAnimation.h" // for SMX_LightsAnimation_SetAuto using namespace std; using namespace SMX; @@ -41,6 +42,9 @@ SMX_API void SMX_Start(SMXUpdateCallback callback, void *pUser) SMX_API void SMX_Stop() { + // If lights animation is running, shut it down first. + SMX_LightsAnimation_SetAuto(false); + SMXManager::g_pSMX.reset(); } diff --git a/sdk/Windows/SMX.vcxproj b/sdk/Windows/SMX.vcxproj index 3b648ea..d238052 100644 --- a/sdk/Windows/SMX.vcxproj +++ b/sdk/Windows/SMX.vcxproj @@ -22,6 +22,7 @@ + @@ -34,6 +35,7 @@ + {C5FC0823-9896-4B7C-BFE1-B60DB671A462} diff --git a/sdk/Windows/SMX.vcxproj.filters b/sdk/Windows/SMX.vcxproj.filters index 68d2ac6..dca4a22 100644 --- a/sdk/Windows/SMX.vcxproj.filters +++ b/sdk/Windows/SMX.vcxproj.filters @@ -40,6 +40,9 @@ Source Files + + Source Files + @@ -72,5 +75,8 @@ Source Files + + Source Files + diff --git a/sdk/Windows/SMXPanelAnimation.cpp b/sdk/Windows/SMXPanelAnimation.cpp new file mode 100644 index 0000000..9d042b4 --- /dev/null +++ b/sdk/Windows/SMXPanelAnimation.cpp @@ -0,0 +1,416 @@ +#include "SMXPanelAnimation.h" +#include "SMXManager.h" +#include "SMXDevice.h" +#include "SMXThread.h" +using namespace std; +using namespace SMX; + +namespace { + Mutex g_Lock; +} + +#define LIGHTS_PER_PANEL 16 + +// XXX: go to sleep if there are no pads connected + +struct AnimationState +{ + SMXPanelAnimation animation; + + // Seconds into the animation: + float fTime = 0; + + // The currently displayed frame: + int iCurrentFrame = 0; + + bool bPlaying = false; + + double m_fLastUpdateTime = -1; + + // Return the current animation frame. + const vector &GetAnimationFrame() const + { + // If we're not playing, return an empty array. As a sanity check, do this + // if the frame is out of bounds too. + if(!bPlaying || iCurrentFrame >= animation.m_aPanelGraphics.size()) + { + static vector dummy; + return dummy; + } + + return animation.m_aPanelGraphics[iCurrentFrame]; + } + + // Start the animation if it's not playing. + void Play() + { + bPlaying = true; + } + + // Stop and disable the animation. + void Stop() + { + bPlaying = false; + Rewind(); + } + + // Reset to the first frame. + void Rewind() + { + fTime = 0; + iCurrentFrame = 0; + } + + // Advance the animation by fSeconds. + void Update() + { + // fSeconds is the time since the last update: + double fNow = SMX::GetMonotonicTime(); + double fSeconds = m_fLastUpdateTime == -1? 0: (fNow - m_fLastUpdateTime); + m_fLastUpdateTime = fNow; + + if(!bPlaying || animation.m_aPanelGraphics.empty()) + return; + + // If the current frame is past the end, a new animation was probably + // loaded. + if(iCurrentFrame >= animation.m_aPanelGraphics.size()) + Rewind(); + + // Advance time. + fTime += fSeconds; + + // If we're still on this frame, we're done. + float fFrameDuration = animation.m_iFrameDurations[iCurrentFrame]; + if(fTime - 0.00001f < fFrameDuration) + return; + + // If we've passed the end of the frame, move to the next frame. Don't + // skip frames if we're updating too quickly. + fTime -= fFrameDuration; + if(fTime > 0) + fTime = 0; + + // Advance the frame. + iCurrentFrame++; + + // If we're at the end of the frame, rewind to the loop frame. + if(iCurrentFrame == animation.m_aPanelGraphics.size()) + iCurrentFrame = animation.m_iLoopFrame; + } +}; + +struct AnimationStateForPad +{ + // asLightsData is an array of lights data to send to the pad and graphic + // is an animation graphic. Overlay graphic on top of the lights. + void OverlayLights(char *asLightsData, const vector &graphic) const + { + // Stop if this graphic isn't loaded or is paused. + if(graphic.empty()) + return; + + for(int i = 0; i < graphic.size(); ++i) + { + if(i >= LIGHTS_PER_PANEL) + return; + + // If this color is transparent, leave the released animation alone. + if(graphic[i].color[3] == 0) + continue; + + asLightsData[i*3+0] = graphic[i].color[0]; + asLightsData[i*3+1] = graphic[i].color[1]; + asLightsData[i*3+2] = graphic[i].color[2]; + } + } + + // Return the command to set the current animation state as pad lights. + string GetLightsCommand(int iPadState, const SMXConfig &config) const + { + g_Lock.AssertLockedByCurrentThread(); + + // If AutoLightingUsePressedAnimations is set, use lights animations. + // If it's not (the config tool is set to step color), mimic the built-in + // step color behavior instead of using pressed animations. Any released + // animation will always be used. + bool bUsePressedAnimations = config.flags & PlatformFlags_AutoLightingUsePressedAnimations; + + const int iBytesPerPanel = LIGHTS_PER_PANEL*3; + const int iTotalLights = 9*iBytesPerPanel; + string result(iTotalLights, 0); + + for(int panel = 0; panel < 9; ++panel) + { + // The portion of lights data for this panel: + char *out = &result[panel*iBytesPerPanel]; + + // Add the released animation, then overlay the pressed animation if we're pressed. + OverlayLights(out, animations[panel][SMX_LightsType_Released].GetAnimationFrame()); + bool bPressed = bool(iPadState & (1 << panel)); + if(bPressed && bUsePressedAnimations) + OverlayLights(out, animations[panel][SMX_LightsType_Pressed].GetAnimationFrame()); + else if(bPressed && !bUsePressedAnimations) + { + // Light all LEDs on this panel using stepColor. + double LightsScaleFactor = 0.666666f; + const uint8_t *color = &config.stepColor[panel*3]; + + for(int light = 0; light < LIGHTS_PER_PANEL; ++light) + { + for(int i = 0; i < 3; ++i) + { + // stepColor is scaled to the 0-170 range. Scale it back to the 0-255 range. + // User applications don't need to worry about this since they normally don't + // need to care about stepColor. + uint8_t c = color[i]; + c = (uint8_t) lrintf(min(255.0f, c / LightsScaleFactor)); + out[light*3+i] = c; + } + } + } + } + + return result; + } + + // State for both animations on each panel: + AnimationState animations[9][NUM_SMX_LightsType]; +}; + +namespace +{ + // Animations and animation states for both pads. + AnimationStateForPad pad_states[2]; +} + +namespace { + // Given a 23x24 graphic frame and a panel number, return an array of 25 colors, containing + // each light in the order it's sent to the master controller. + void ConvertToPanelGraphic(const SMXGif::GIFImage &src, vector &dst, int panel) + { + vector> graphic_positions = { + { 0,0 }, + { 1,0 }, + { 2,0 }, + { 0,1 }, + { 1,1 }, + { 2,1 }, + { 0,2 }, + { 1,2 }, + { 2,2 }, + }; + + dst.clear(); + + // The top-left corner for this panel: + int x = graphic_positions[panel].first * 8; + int y = graphic_positions[panel].second * 8; + + // Add the 4x4 grid first. + for(int dy = 0; dy < 4; ++dy) + for(int dx = 0; dx < 4; ++dx) + dst.push_back(src.get(x+dx*2, y+dy*2)); + + // Add the 3x3 grid. + for(int dy = 0; dy < 3; ++dy) + for(int dx = 0; dx < 3; ++dx) + dst.push_back(src.get(x+dx*2+1, y+dy*2+1)); + } +} + +// Return the SMXPanelAnimation. The rest of the animation state is internal. +SMXPanelAnimation SMXPanelAnimation::GetLoadedAnimation(int pad, int panel, SMX_LightsType type) +{ + g_Lock.AssertNotLockedByCurrentThread(); + LockMutex L(g_Lock); + return pad_states[pad].animations[panel][type].animation; +} + +// Load an array of animation frames as a panel animation. Each frame must +// be 14x15. +void SMXPanelAnimation::Load(const vector &frames, int panel) +{ + m_aPanelGraphics.clear(); + m_iFrameDurations.clear(); + m_iLoopFrame = -1; + + for(int frame_no = 0; frame_no < frames.size(); ++frame_no) + { + const SMXGif::SMXGifFrame &gif_frame = frames[frame_no]; + + // If the bottom-left pixel is opaque, this is the loop frame, which marks the + // frame the animation should start at after a loop. This is global to the + // animation, not specific to each panel. + if(gif_frame.frame.get(0, gif_frame.frame.height-1).color[3] != 0) + { + // We shouldn't see more than one of these. If we do, use the first. + if(m_iLoopFrame != -1) + m_iLoopFrame = frame_no; + } + + // Extract this frame. + vector panel_graphic; + ConvertToPanelGraphic(gif_frame.frame, panel_graphic, panel); + m_aPanelGraphics.push_back(panel_graphic); + + // GIFs have a very low-resolution duration field, with 10ms units. + // The panels run at 30 FPS internally, or 33 1/3 ms, but GIF can only + // represent 30ms or 40ms. Most applications will probably output 30, + // but snap both 30ms and 40ms to exactly 30 FPS to make sure animations + // that are meant to run at native framerate do. + float seconds; + if(gif_frame.milliseconds == 30 || gif_frame.milliseconds == 40) + seconds = 1 / 30.0f; + else + seconds = gif_frame.milliseconds / 1000.0; + + m_iFrameDurations.push_back(seconds); + } + + // By default, loop back to the first frame. + if(m_iLoopFrame == -1) + m_iLoopFrame = 0; +} + +// Load a GIF into SMXLoadedPanelAnimations::animations. +bool SMX_LightsAnimation_Load(const char *gif, int size, int pad, SMX_LightsType type, const char **error) +{ + // Parse the GIF. + string buf(gif, size); + vector frames; + if(!SMXGif::DecodeGIF(buf, frames) || frames.empty()) + { + *error = "The GIF couldn't be read."; + return false; + } + + // Check the dimensions of the image. We only need to check the first, the + // others will always have the same size. + if(frames[0].width != 14 || frames[0].height != 15) + { + *error = "The GIF must be 14x15."; + return false; + } + + // Lock while we access pad_states. + g_Lock.AssertNotLockedByCurrentThread(); + LockMutex L(g_Lock); + + // Load the animation for each panel into SMXPanelAnimations. + for(int panel = 0; panel < 9; ++panel) + { + SMXPanelAnimation &animation = pad_states[pad].animations[panel][type].animation; + animation.Load(frames, panel); + } + + return true; +} + +// A thread to handle setting light animations. We do this in a separate +// thread rather than in the SMXManager thread so this can be treated as +// if it's external application thread, and it's making normal threaded +// calls to SetLights. +class PanelAnimationThread: public SMXThread +{ +public: + static shared_ptr g_pSingleton; + PanelAnimationThread(): + SMXThread(g_Lock) + { + Start("SMX light animations"); + } + +private: + void ThreadMain() + { + m_Lock.Lock(); + + // Update lights at 30 FPS. + const int iDelayMS = 33; + + while(!m_bShutdown) + { + // Run a single panel lights update. + UpdateLights(); + + // Wait up to 30 FPS, or until we're signalled. We can only be signalled + // if we're shutting down, so we don't need to worry about partial frame + // delays. + m_Event.Wait(iDelayMS); + } + + m_Lock.Unlock(); + } + + // Return lights for the given pad and pad state, using the loaded panel animations. + void GetCurrentLights(string &asLightsDataOut, int pad, int iPadState) + { + m_Lock.AssertLockedByCurrentThread(); + + // Get this pad's configuration. + SMXConfig config; + if(!SMXManager::g_pSMX->GetDevice(pad)->GetConfig(config)) + return; + + AnimationStateForPad &pad_state = pad_states[pad]; + + // Make sure the correct animations are playing. + for(int panel = 0; panel < 9; ++panel) + { + // The released animation is always playing. + pad_state.animations[panel][SMX_LightsType_Released].Play(); + + // The pressed animation only plays while the button is pressed, + // and rewind when it's released. + bool bPressed = iPadState & (1 << panel); + if(bPressed) + pad_state.animations[panel][SMX_LightsType_Pressed].Play(); + else + pad_state.animations[panel][SMX_LightsType_Pressed].Stop(); + } + + // Set the current state. + asLightsDataOut = pad_state.GetLightsCommand(iPadState, config); + + // Advance animations. + for(int panel = 0; panel < 9; ++panel) + { + for(auto &animation_state: pad_state.animations[panel]) + animation_state.Update(); + } + } + + // Run a single light animation update. + void UpdateLights() + { + string asLightsData[2]; + for(int pad = 0; pad < 2; pad++) + { + int iPadState = SMXManager::g_pSMX->GetDevice(pad)->GetInputState(); + GetCurrentLights(asLightsData[pad], pad, iPadState); + } + + // Update lights. + SMXManager::g_pSMX->SetLights(asLightsData); + } +}; + +void SMX_LightsAnimation_SetAuto(bool enable) +{ + if(!enable) + { + // If we're turning off, shut down the thread if it's running. + if(PanelAnimationThread::g_pSingleton) + PanelAnimationThread::g_pSingleton->Shutdown(); + PanelAnimationThread::g_pSingleton.reset(); + return; + } + + // Create the animation thread if it's not already running. + if(PanelAnimationThread::g_pSingleton) + return; + PanelAnimationThread::g_pSingleton.reset(new PanelAnimationThread()); +} + +shared_ptr PanelAnimationThread::g_pSingleton; diff --git a/sdk/Windows/SMXPanelAnimation.h b/sdk/Windows/SMXPanelAnimation.h new file mode 100644 index 0000000..000d18a --- /dev/null +++ b/sdk/Windows/SMXPanelAnimation.h @@ -0,0 +1,50 @@ +#ifndef SMXPanelAnimation_h +#define SMXPanelAnimation_h + +#include +#include "SMXGif.h" + +enum SMX_LightsType +{ + SMX_LightsType_Released, // animation while panels are released + SMX_LightsType_Pressed, // animation while panel is pressed + NUM_SMX_LightsType, +}; + +// SMXPanelAnimation holds an animation, with graphics for a single panel. +class SMXPanelAnimation +{ +public: + // Return the animation loaded by SMX_LightsAnimation_Load. + static SMXPanelAnimation GetLoadedAnimation(int pad, int panel, SMX_LightsType type); + + void Load(const std::vector &frames, int panel); + + // The high-level animated GIF frames: + std::vector> m_aPanelGraphics; + + // The animation starts on frame 0. When it reaches the end, it loops + // back to this frame. + int m_iLoopFrame = 0; + + // The duration of each frame in seconds. + std::vector m_iFrameDurations; +}; + +// For SMX_API: +#include "../SMX.h" + +// High-level interface for C# bindings: +// +// Load an animated GIF as a panel animation. pad is the pad this animation is for (0 or 1), +// and type is which animation this is for. Any previously loaded animation will be replaced. +// On error, false is returned and error is set to a plain-text error message which is valid +// until the next call. +SMX_API bool SMX_LightsAnimation_Load(const char *gif, int size, int pad, SMX_LightsType type, const char **error); + +// Enable or disable automatically handling lights animations. If enabled, any animations +// loaded with SMX_LightsAnimation_Load will run automatically as long as the SDK is loaded. +// XXX: should we automatically disable SMX_SetLights when this is enabled? +SMX_API void SMX_LightsAnimation_SetAuto(bool enable); + +#endif diff --git a/smx-config/SMX.cs b/smx-config/SMX.cs index 0367bba..a4942d0 100644 --- a/smx-config/SMX.cs +++ b/smx-config/SMX.cs @@ -21,6 +21,11 @@ namespace SMX private Byte dummy; }; + // Bits for SMXConfig::flags. + public enum SMXConfigFlags { + SMXConfigFlags_AutoLightingUsePressedAnimations = 1 << 0, + }; + [StructLayout(LayoutKind.Sequential, Pack=1)] public struct SMXConfig { public Byte unused1, unused2; @@ -60,8 +65,29 @@ namespace SMX public Byte panelThreshold6Low, panelThreshold6High; public Byte panelThreshold8Low, panelThreshold8High; + // Master delay debouncing (version >= 3). If enabled, this will add a + // corresponding delay to inputs, which the game needs to compensate for. + // This is disabled by default. + public UInt16 debounceDelayMs; + + // Packed flags (SMXConfigFlags). + public Byte flags; + + // It would be simpler to set flags to [MarshalAs(UnmanagedType.U8)], but + // that doesn't work. + public SMXConfigFlags configFlags { + get { + return (SMXConfigFlags) flags; + } + + set { + flags = (Byte) value; + } + } + // Pad this struct to exactly 250 bytes. [MarshalAs(UnmanagedType.ByValArray, SizeConst = 166)] + public Byte[] padding; // enabledSensors is a mask of which panels are enabled. Return this as an array @@ -248,11 +274,15 @@ namespace SMX private static extern bool SMX_GetTestData(int pad, out SMXSensorTestModeData data); [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] private static extern bool SMX_SetLights(byte[] buf); - [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] - private static extern bool SMX_ReenableAutoLights(); [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] private static extern IntPtr SMX_Version(); + public enum LightsType + { + LightsType_Released, // animation while panels are released + LightsType_Pressed, // animation while panel is pressed + }; + public static string Version() { if(!DLLAvailable()) return ""; @@ -420,10 +450,38 @@ namespace SMX SMX_SetLights(buf); } - public static void ReenableAutoLights() + // SMXPanelAnimation + [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + [return:MarshalAs(UnmanagedType.I1)] + private static extern bool SMX_LightsAnimation_Load(byte[] buf, int size, int pad, int type, out IntPtr error); + [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + private static extern void SMX_LightsAnimation_SetAuto(bool enable); + + public static bool LightsAnimation_Load(byte[] buf, int pad, LightsType type, out string error) + { + if(!DLLAvailable()) + { + error = "SMX.DLL not available"; + return false; + } + + error = ""; + IntPtr error_pointer; + bool result = SMX_LightsAnimation_Load(buf, buf.Length, pad, (int) type, out error_pointer); + if(!result) + { + // SMX_LightsAnimation_Load takes a char **error, which is set to the error + // string. + error = Marshal.PtrToStringAnsi(error_pointer); + } + + return result; + } + + public static void LightsAnimation_SetAuto(bool enable) { if(!DLLAvailable()) return; - SMX_ReenableAutoLights(); + SMX_LightsAnimation_SetAuto(enable); } } }