프로젝트 소스 공개
This commit is contained in:
Executable
+28
@@ -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;
|
||||
}
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/zsh
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=${0:A:h}
|
||||
project_root=${script_dir:h:h}
|
||||
sparkle_root="$project_root/ThirdParty/Sparkle/2.9.4"
|
||||
source_file="$script_dir/project_quint_updater.mm"
|
||||
bundle_root="$project_root/Assets/ProjectQuint/Plugins/macOS/ProjectQuintUpdater.bundle"
|
||||
output_file="$bundle_root/Contents/MacOS/ProjectQuintUpdater"
|
||||
|
||||
mkdir -p "${output_file:h}"
|
||||
/usr/bin/xcrun --sdk macosx clang++ \
|
||||
-std=c++17 \
|
||||
-O2 \
|
||||
-DNDEBUG \
|
||||
-fobjc-arc \
|
||||
-fvisibility=hidden \
|
||||
-mmacosx-version-min=12.0 \
|
||||
-arch arm64 \
|
||||
-arch x86_64 \
|
||||
-dynamiclib \
|
||||
-install_name '@rpath/ProjectQuintUpdater' \
|
||||
-Wl,-rpath,'@loader_path/../../../../Frameworks' \
|
||||
-F "$sparkle_root" \
|
||||
-framework AppKit \
|
||||
-framework Foundation \
|
||||
-framework Sparkle \
|
||||
"$source_file" \
|
||||
-o "$output_file"
|
||||
|
||||
/usr/bin/lipo "$output_file" -verify_arch arm64 x86_64
|
||||
/usr/bin/otool -L "$output_file" | /usr/bin/grep -Fq "@rpath/Sparkle.framework/Versions/B/Sparkle"
|
||||
/usr/bin/otool -l "$output_file" | /usr/bin/grep -Fq "@loader_path/../../../../Frameworks"
|
||||
/usr/bin/codesign --force --sign - "$bundle_root"
|
||||
/usr/bin/codesign --verify --strict "$bundle_root"
|
||||
echo "Built Universal ProjectQuintUpdater: $output_file"
|
||||
@@ -0,0 +1,110 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <Sparkle/Sparkle.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#if defined(__GNUC__)
|
||||
#define PQ_EXPORT extern "C" __attribute__((visibility("default")))
|
||||
#else
|
||||
#define PQ_EXPORT extern "C"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
SPUStandardUpdaterController *s_updaterController = nil;
|
||||
std::string s_lastError;
|
||||
|
||||
void SetError(NSString *message)
|
||||
{
|
||||
s_lastError = message == nil ? std::string() : std::string(message.UTF8String);
|
||||
}
|
||||
|
||||
bool RequireMainThread()
|
||||
{
|
||||
if ([NSThread isMainThread])
|
||||
return true;
|
||||
SetError(@"Sparkle updater calls must run on the macOS main thread.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
PQ_EXPORT bool PQUpdaterInitialize()
|
||||
{
|
||||
@autoreleasepool
|
||||
{
|
||||
if (!RequireMainThread())
|
||||
return false;
|
||||
if (s_updaterController != nil)
|
||||
return true;
|
||||
|
||||
@try
|
||||
{
|
||||
s_updaterController = [[SPUStandardUpdaterController alloc]
|
||||
initWithStartingUpdater:YES
|
||||
updaterDelegate:nil
|
||||
userDriverDelegate:nil];
|
||||
if (s_updaterController == nil)
|
||||
{
|
||||
SetError(@"Sparkle updater controller could not be created.");
|
||||
return false;
|
||||
}
|
||||
s_lastError.clear();
|
||||
return true;
|
||||
}
|
||||
@catch (NSException *exception)
|
||||
{
|
||||
SetError(exception.reason ?: exception.name);
|
||||
s_updaterController = nil;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PQ_EXPORT bool PQUpdaterCanCheckForUpdates()
|
||||
{
|
||||
@autoreleasepool
|
||||
{
|
||||
if (!RequireMainThread() || s_updaterController == nil)
|
||||
return false;
|
||||
return s_updaterController.updater.canCheckForUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
PQ_EXPORT bool PQUpdaterCheckForUpdates()
|
||||
{
|
||||
@autoreleasepool
|
||||
{
|
||||
if (!RequireMainThread())
|
||||
return false;
|
||||
if (s_updaterController == nil)
|
||||
{
|
||||
SetError(@"Sparkle updater has not been initialized.");
|
||||
return false;
|
||||
}
|
||||
if (!s_updaterController.updater.canCheckForUpdates)
|
||||
{
|
||||
SetError(@"Sparkle is already checking for an update.");
|
||||
return false;
|
||||
}
|
||||
|
||||
[s_updaterController checkForUpdates:nil];
|
||||
s_lastError.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
PQ_EXPORT void PQUpdaterShutdown()
|
||||
{
|
||||
@autoreleasepool
|
||||
{
|
||||
if (!RequireMainThread())
|
||||
return;
|
||||
s_updaterController = nil;
|
||||
s_lastError.clear();
|
||||
}
|
||||
}
|
||||
|
||||
PQ_EXPORT const char *PQUpdaterGetLastError()
|
||||
{
|
||||
return s_lastError.c_str();
|
||||
}
|
||||
Reference in New Issue
Block a user