프로젝트 소스 공개

This commit is contained in:
2026-09-06 17:11:40 +09:00
commit 0f0431de5a
676 changed files with 106118 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
#!/bin/zsh
set -euo pipefail
script_dir=${0:A:h}
project_root=${script_dir:h:h}
source_file="$script_dir/project_quint_input.cpp"
bundle_root="$project_root/Assets/ProjectQuint/Plugins/macOS/ProjectQuintInput.bundle"
output_file="$bundle_root/Contents/MacOS/ProjectQuintInput"
mkdir -p "${output_file:h}"
/usr/bin/xcrun --sdk macosx clang++ \
-std=c++17 \
-O3 \
-DNDEBUG \
-fvisibility=hidden \
-mmacosx-version-min=12.0 \
-arch arm64 \
-arch x86_64 \
-dynamiclib \
-install_name '@rpath/ProjectQuintInput' \
-framework ApplicationServices \
"$source_file" \
-o "$output_file"
/usr/bin/lipo "$output_file" -verify_arch arm64 x86_64
/usr/bin/codesign --force --sign - "$bundle_root"
/usr/bin/codesign --verify --strict "$bundle_root"
echo "Built Universal ProjectQuintInput: $output_file"
@@ -0,0 +1,283 @@
#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;
}