#include #include #include #include #include #include #include #include #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 keyCodes{}; std::array previousStates{}; std::array events{}; std::atomic writeIndex{0}; std::atomic readIndex{0}; std::atomic running{false}; std::atomic enabled{false}; std::atomic sampleCount{0}; std::atomic droppedEventCount{0}; std::atomic lateWakeupCount{0}; std::atomic 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(ticks) * static_cast(timebase.numer) / static_cast(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(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(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(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(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; }