284 lines
8.7 KiB
C++
284 lines
8.7 KiB
C++
#include <ApplicationServices/ApplicationServices.h>
|
|
#include <mach/mach_time.h>
|
|
#include <pthread/qos.h>
|
|
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <thread>
|
|
|
|
#define PQ_EXPORT extern "C" __attribute__((visibility("default")))
|
|
|
|
namespace
|
|
{
|
|
constexpr int32_t MaxBindings = 16;
|
|
constexpr uint32_t EventCapacity = 4096;
|
|
|
|
struct NativeInputEvent
|
|
{
|
|
int32_t bindingIndex;
|
|
int32_t pressed;
|
|
double timestampSeconds;
|
|
};
|
|
|
|
struct NativePollingSnapshot
|
|
{
|
|
int32_t targetHz;
|
|
int32_t bindingCount;
|
|
uint64_t sampleCount;
|
|
uint64_t droppedEventCount;
|
|
uint64_t lateWakeupCount;
|
|
double elapsedSeconds;
|
|
};
|
|
|
|
std::array<int32_t, MaxBindings> keyCodes{};
|
|
std::array<bool, MaxBindings> previousStates{};
|
|
std::array<NativeInputEvent, EventCapacity> events{};
|
|
std::atomic<uint32_t> writeIndex{0};
|
|
std::atomic<uint32_t> readIndex{0};
|
|
std::atomic<bool> running{false};
|
|
std::atomic<bool> enabled{false};
|
|
std::atomic<uint64_t> sampleCount{0};
|
|
std::atomic<uint64_t> droppedEventCount{0};
|
|
std::atomic<uint64_t> lateWakeupCount{0};
|
|
std::atomic<uint64_t> activeElapsedTicks{0};
|
|
std::thread pollingThread;
|
|
int32_t configuredHz = 1000;
|
|
int32_t configuredBindingCount = 0;
|
|
mach_timebase_info_data_t timebase{};
|
|
char lastError[256]{};
|
|
|
|
double TicksToSeconds(uint64_t ticks)
|
|
{
|
|
return static_cast<double>(ticks) * static_cast<double>(timebase.numer) /
|
|
static_cast<double>(timebase.denom) / 1'000'000'000.0;
|
|
}
|
|
|
|
uint64_t FrequencyToTicks(int32_t frequency)
|
|
{
|
|
const long double nanoseconds = 1'000'000'000.0L / frequency;
|
|
const long double ticks = nanoseconds * timebase.denom / timebase.numer;
|
|
return static_cast<uint64_t>(ticks < 1.0L ? 1.0L : ticks);
|
|
}
|
|
|
|
void SetError(const char* message)
|
|
{
|
|
std::strncpy(lastError, message == nullptr ? "Unknown native input error." : message,
|
|
sizeof(lastError) - 1);
|
|
lastError[sizeof(lastError) - 1] = '\0';
|
|
}
|
|
|
|
void ClearQueue()
|
|
{
|
|
readIndex.store(0, std::memory_order_release);
|
|
writeIndex.store(0, std::memory_order_release);
|
|
}
|
|
|
|
void Enqueue(int32_t bindingIndex, bool pressed, uint64_t timestamp)
|
|
{
|
|
const uint32_t write = writeIndex.load(std::memory_order_relaxed);
|
|
const uint32_t next = (write + 1) % EventCapacity;
|
|
if (next == readIndex.load(std::memory_order_acquire))
|
|
{
|
|
droppedEventCount.fetch_add(1, std::memory_order_relaxed);
|
|
return;
|
|
}
|
|
|
|
events[write] = NativeInputEvent{
|
|
bindingIndex,
|
|
pressed ? 1 : 0,
|
|
TicksToSeconds(timestamp),
|
|
};
|
|
writeIndex.store(next, std::memory_order_release);
|
|
}
|
|
|
|
void CaptureBaseline()
|
|
{
|
|
for (int32_t index = 0; index < configuredBindingCount; ++index)
|
|
{
|
|
previousStates[index] = CGEventSourceKeyState(
|
|
kCGEventSourceStateHIDSystemState,
|
|
static_cast<CGKeyCode>(keyCodes[index]));
|
|
}
|
|
}
|
|
|
|
void PollingLoop()
|
|
{
|
|
pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0);
|
|
const uint64_t period = FrequencyToTicks(configuredHz);
|
|
uint64_t nextDeadline = mach_absolute_time();
|
|
bool wasEnabled = false;
|
|
uint64_t previousActiveSample = 0;
|
|
|
|
while (running.load(std::memory_order_acquire))
|
|
{
|
|
const bool shouldSample = enabled.load(std::memory_order_acquire);
|
|
const uint64_t sampledAt = mach_absolute_time();
|
|
if (shouldSample)
|
|
{
|
|
if (!wasEnabled)
|
|
{
|
|
CaptureBaseline();
|
|
ClearQueue();
|
|
wasEnabled = true;
|
|
previousActiveSample = sampledAt;
|
|
}
|
|
else
|
|
{
|
|
activeElapsedTicks.fetch_add(
|
|
sampledAt - previousActiveSample,
|
|
std::memory_order_relaxed);
|
|
previousActiveSample = sampledAt;
|
|
for (int32_t index = 0; index < configuredBindingCount; ++index)
|
|
{
|
|
const bool pressed = CGEventSourceKeyState(
|
|
kCGEventSourceStateHIDSystemState,
|
|
static_cast<CGKeyCode>(keyCodes[index]));
|
|
if (pressed == previousStates[index])
|
|
continue;
|
|
previousStates[index] = pressed;
|
|
Enqueue(index, pressed, sampledAt);
|
|
}
|
|
}
|
|
sampleCount.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
else
|
|
{
|
|
wasEnabled = false;
|
|
}
|
|
|
|
nextDeadline += period;
|
|
const uint64_t afterSample = mach_absolute_time();
|
|
if (nextDeadline <= afterSample)
|
|
{
|
|
const uint64_t missedPeriods = (afterSample - nextDeadline) / period + 1;
|
|
nextDeadline += missedPeriods * period;
|
|
lateWakeupCount.fetch_add(missedPeriods, std::memory_order_relaxed);
|
|
}
|
|
mach_wait_until(nextDeadline);
|
|
}
|
|
}
|
|
|
|
void StopInternal()
|
|
{
|
|
enabled.store(false, std::memory_order_release);
|
|
running.store(false, std::memory_order_release);
|
|
if (pollingThread.joinable())
|
|
pollingThread.join();
|
|
ClearQueue();
|
|
}
|
|
}
|
|
|
|
PQ_EXPORT int32_t PQInputStart(int32_t frequency, const int32_t* bindings, int32_t bindingCount)
|
|
{
|
|
StopInternal();
|
|
lastError[0] = '\0';
|
|
if (frequency != 1000 && frequency != 2000 && frequency != 4000 && frequency != 8000)
|
|
{
|
|
SetError("Polling frequency must be 1000, 2000, 4000, or 8000 Hz.");
|
|
return 0;
|
|
}
|
|
if (bindings == nullptr || bindingCount <= 0 || bindingCount > MaxBindings)
|
|
{
|
|
SetError("Native polling requires between 1 and 16 key bindings.");
|
|
return 0;
|
|
}
|
|
|
|
if (timebase.denom == 0 && mach_timebase_info(&timebase) != KERN_SUCCESS)
|
|
{
|
|
SetError("mach_timebase_info failed.");
|
|
return 0;
|
|
}
|
|
|
|
for (int32_t index = 0; index < bindingCount; ++index)
|
|
{
|
|
if (bindings[index] < 0 || bindings[index] > UINT16_MAX)
|
|
{
|
|
SetError("A configured macOS virtual key code is invalid.");
|
|
return 0;
|
|
}
|
|
keyCodes[index] = bindings[index];
|
|
previousStates[index] = false;
|
|
}
|
|
|
|
configuredHz = frequency;
|
|
configuredBindingCount = bindingCount;
|
|
sampleCount.store(0, std::memory_order_release);
|
|
droppedEventCount.store(0, std::memory_order_release);
|
|
lateWakeupCount.store(0, std::memory_order_release);
|
|
activeElapsedTicks.store(0, std::memory_order_release);
|
|
ClearQueue();
|
|
running.store(true, std::memory_order_release);
|
|
enabled.store(true, std::memory_order_release);
|
|
try
|
|
{
|
|
pollingThread = std::thread(PollingLoop);
|
|
}
|
|
catch (...)
|
|
{
|
|
running.store(false, std::memory_order_release);
|
|
enabled.store(false, std::memory_order_release);
|
|
SetError("Could not create the native polling thread.");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
PQ_EXPORT void PQInputStop()
|
|
{
|
|
StopInternal();
|
|
}
|
|
|
|
PQ_EXPORT void PQInputSetEnabled(int32_t value)
|
|
{
|
|
enabled.store(value != 0, std::memory_order_release);
|
|
}
|
|
|
|
PQ_EXPORT int32_t PQInputTryDequeue(NativeInputEvent* output)
|
|
{
|
|
if (output == nullptr)
|
|
return 0;
|
|
const uint32_t read = readIndex.load(std::memory_order_relaxed);
|
|
if (read == writeIndex.load(std::memory_order_acquire))
|
|
return 0;
|
|
*output = events[read];
|
|
readIndex.store((read + 1) % EventCapacity, std::memory_order_release);
|
|
return 1;
|
|
}
|
|
|
|
PQ_EXPORT int32_t PQInputIsKeyDown(int32_t keyCode)
|
|
{
|
|
if (keyCode < 0 || keyCode > UINT16_MAX)
|
|
return 0;
|
|
return CGEventSourceKeyState(
|
|
kCGEventSourceStateHIDSystemState,
|
|
static_cast<CGKeyCode>(keyCode)) ? 1 : 0;
|
|
}
|
|
|
|
PQ_EXPORT double PQInputNowSeconds()
|
|
{
|
|
if (timebase.denom == 0)
|
|
mach_timebase_info(&timebase);
|
|
return TicksToSeconds(mach_absolute_time());
|
|
}
|
|
|
|
PQ_EXPORT void PQInputGetSnapshot(NativePollingSnapshot* output)
|
|
{
|
|
if (output == nullptr)
|
|
return;
|
|
*output = NativePollingSnapshot{
|
|
configuredHz,
|
|
configuredBindingCount,
|
|
sampleCount.load(std::memory_order_acquire),
|
|
droppedEventCount.load(std::memory_order_acquire),
|
|
lateWakeupCount.load(std::memory_order_acquire),
|
|
TicksToSeconds(activeElapsedTicks.load(std::memory_order_acquire)),
|
|
};
|
|
}
|
|
|
|
PQ_EXPORT const char* PQInputGetLastError()
|
|
{
|
|
return lastError;
|
|
}
|