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);
}
}
}