begin macos port
This commit is contained in:
343
src/audio/audio_coreaudio.cpp
Normal file
343
src/audio/audio_coreaudio.cpp
Normal file
@@ -0,0 +1,343 @@
|
||||
#include "audio/audio.h"
|
||||
#include <AudioToolbox/AudioToolbox.h>
|
||||
#include <CoreAudio/CoreAudio.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
#define AUDIO_MAX_DEVICES 32
|
||||
#define AUDIO_MAX_CHANNELS 32
|
||||
#define AUDIO_TEST_TONE_HZ 440.0
|
||||
#define AUDIO_TEST_TONE_SEC 2.0
|
||||
#define AUDIO_PI 3.14159265358979323846
|
||||
|
||||
struct CoreAudioDeviceInfo {
|
||||
AudioDeviceID device_id;
|
||||
};
|
||||
|
||||
struct AudioEngine {
|
||||
AudioDeviceInfo devices[AUDIO_MAX_DEVICES];
|
||||
CoreAudioDeviceInfo ca_devices[AUDIO_MAX_DEVICES];
|
||||
int32_t device_count;
|
||||
|
||||
AUGraph graph;
|
||||
AudioUnit output_unit;
|
||||
int32_t active_device_index;
|
||||
double sample_rate;
|
||||
int32_t num_channels;
|
||||
|
||||
// Test tone state (accessed from audio render thread)
|
||||
_Atomic int32_t test_tone_active;
|
||||
_Atomic int32_t test_tone_samples_remaining;
|
||||
double test_tone_phase;
|
||||
};
|
||||
|
||||
////////////////////////////////
|
||||
// Audio render callback
|
||||
|
||||
static AudioEngine *g_audio_engine = nullptr;
|
||||
|
||||
static OSStatus audio_render_callback(void *inRefCon,
|
||||
AudioUnitRenderActionFlags *ioActionFlags,
|
||||
const AudioTimeStamp *inTimeStamp,
|
||||
UInt32 inBusNumber,
|
||||
UInt32 inNumberFrames,
|
||||
AudioBufferList *ioData)
|
||||
{
|
||||
(void)inRefCon; (void)ioActionFlags; (void)inTimeStamp; (void)inBusNumber;
|
||||
|
||||
AudioEngine *engine = g_audio_engine;
|
||||
if (!engine) {
|
||||
for (UInt32 buf = 0; buf < ioData->mNumberBuffers; buf++)
|
||||
memset(ioData->mBuffers[buf].mData, 0, ioData->mBuffers[buf].mDataByteSize);
|
||||
return noErr;
|
||||
}
|
||||
|
||||
int32_t tone_active = atomic_load(&engine->test_tone_active);
|
||||
|
||||
if (!tone_active) {
|
||||
for (UInt32 buf = 0; buf < ioData->mNumberBuffers; buf++)
|
||||
memset(ioData->mBuffers[buf].mData, 0, ioData->mBuffers[buf].mDataByteSize);
|
||||
return noErr;
|
||||
}
|
||||
|
||||
double phase = engine->test_tone_phase;
|
||||
double phase_inc = 2.0 * AUDIO_PI * AUDIO_TEST_TONE_HZ / engine->sample_rate;
|
||||
int32_t remaining = atomic_load(&engine->test_tone_samples_remaining);
|
||||
int32_t samples_to_gen = (remaining < (int32_t)inNumberFrames) ? remaining : (int32_t)inNumberFrames;
|
||||
|
||||
// CoreAudio with kAudioFormatFlagIsNonInterleaved: each buffer = one channel
|
||||
for (UInt32 buf = 0; buf < ioData->mNumberBuffers; buf++) {
|
||||
float *out = (float *)ioData->mBuffers[buf].mData;
|
||||
double p = phase;
|
||||
for (UInt32 s = 0; s < inNumberFrames; s++) {
|
||||
if ((int32_t)s < samples_to_gen) {
|
||||
out[s] = (float)(sin(p) * 0.5); // -6dB
|
||||
p += phase_inc;
|
||||
if (p >= 2.0 * AUDIO_PI) p -= 2.0 * AUDIO_PI;
|
||||
} else {
|
||||
out[s] = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Advance phase using first channel's traversal
|
||||
phase += phase_inc * samples_to_gen;
|
||||
while (phase >= 2.0 * AUDIO_PI) phase -= 2.0 * AUDIO_PI;
|
||||
engine->test_tone_phase = phase;
|
||||
|
||||
int32_t new_remaining = atomic_fetch_sub(&engine->test_tone_samples_remaining, samples_to_gen);
|
||||
if (new_remaining <= samples_to_gen) {
|
||||
atomic_store(&engine->test_tone_active, 0);
|
||||
atomic_store(&engine->test_tone_samples_remaining, 0);
|
||||
}
|
||||
|
||||
return noErr;
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Device enumeration
|
||||
|
||||
static void enumerate_output_devices(AudioEngine *engine) {
|
||||
engine->device_count = 0;
|
||||
|
||||
AudioObjectPropertyAddress prop = {
|
||||
kAudioHardwarePropertyDevices,
|
||||
kAudioObjectPropertyScopeGlobal,
|
||||
kAudioObjectPropertyElementMain
|
||||
};
|
||||
|
||||
UInt32 data_size = 0;
|
||||
if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &prop, 0, nullptr, &data_size) != noErr)
|
||||
return;
|
||||
|
||||
int device_count = (int)(data_size / sizeof(AudioDeviceID));
|
||||
if (device_count <= 0) return;
|
||||
|
||||
AudioDeviceID *device_ids = (AudioDeviceID *)malloc(data_size);
|
||||
if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &prop, 0, nullptr, &data_size, device_ids) != noErr) {
|
||||
free(device_ids);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < device_count && engine->device_count < AUDIO_MAX_DEVICES; i++) {
|
||||
// Check if device has output channels
|
||||
AudioObjectPropertyAddress stream_prop = {
|
||||
kAudioDevicePropertyStreamConfiguration,
|
||||
kAudioDevicePropertyScopeOutput,
|
||||
kAudioObjectPropertyElementMain
|
||||
};
|
||||
|
||||
UInt32 stream_size = 0;
|
||||
if (AudioObjectGetPropertyDataSize(device_ids[i], &stream_prop, 0, nullptr, &stream_size) != noErr)
|
||||
continue;
|
||||
|
||||
AudioBufferList *buf_list = (AudioBufferList *)malloc(stream_size);
|
||||
if (AudioObjectGetPropertyData(device_ids[i], &stream_prop, 0, nullptr, &stream_size, buf_list) != noErr) {
|
||||
free(buf_list);
|
||||
continue;
|
||||
}
|
||||
|
||||
int output_channels = 0;
|
||||
for (UInt32 b = 0; b < buf_list->mNumberBuffers; b++)
|
||||
output_channels += (int)buf_list->mBuffers[b].mNumberChannels;
|
||||
free(buf_list);
|
||||
|
||||
if (output_channels == 0) continue;
|
||||
|
||||
// Get device name
|
||||
AudioObjectPropertyAddress name_prop = {
|
||||
kAudioObjectPropertyName,
|
||||
kAudioObjectPropertyScopeGlobal,
|
||||
kAudioObjectPropertyElementMain
|
||||
};
|
||||
|
||||
CFStringRef name_ref = nullptr;
|
||||
UInt32 name_size = sizeof(name_ref);
|
||||
if (AudioObjectGetPropertyData(device_ids[i], &name_prop, 0, nullptr, &name_size, &name_ref) != noErr)
|
||||
continue;
|
||||
|
||||
int32_t idx = engine->device_count++;
|
||||
CFStringGetCString(name_ref, engine->devices[idx].name, sizeof(engine->devices[idx].name),
|
||||
kCFStringEncodingUTF8);
|
||||
CFRelease(name_ref);
|
||||
engine->devices[idx].id = idx;
|
||||
engine->ca_devices[idx].device_id = device_ids[i];
|
||||
}
|
||||
|
||||
free(device_ids);
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Public API
|
||||
|
||||
AudioEngine *audio_create(void *hwnd) {
|
||||
(void)hwnd;
|
||||
|
||||
AudioEngine *engine = new AudioEngine();
|
||||
memset(engine, 0, sizeof(*engine));
|
||||
engine->active_device_index = -1;
|
||||
atomic_store(&engine->test_tone_active, 0);
|
||||
atomic_store(&engine->test_tone_samples_remaining, 0);
|
||||
g_audio_engine = engine;
|
||||
|
||||
enumerate_output_devices(engine);
|
||||
return engine;
|
||||
}
|
||||
|
||||
void audio_destroy(AudioEngine *engine) {
|
||||
audio_close_device(engine);
|
||||
if (g_audio_engine == engine) g_audio_engine = nullptr;
|
||||
delete engine;
|
||||
}
|
||||
|
||||
void audio_refresh_devices(AudioEngine *engine) {
|
||||
audio_close_device(engine);
|
||||
enumerate_output_devices(engine);
|
||||
}
|
||||
|
||||
int32_t audio_get_device_count(AudioEngine *engine) {
|
||||
return engine->device_count;
|
||||
}
|
||||
|
||||
AudioDeviceInfo *audio_get_device(AudioEngine *engine, int32_t index) {
|
||||
if (index < 0 || index >= engine->device_count) return nullptr;
|
||||
return &engine->devices[index];
|
||||
}
|
||||
|
||||
bool audio_open_device(AudioEngine *engine, int32_t index) {
|
||||
audio_close_device(engine);
|
||||
|
||||
if (index < 0 || index >= engine->device_count) return false;
|
||||
|
||||
AudioDeviceID device_id = engine->ca_devices[index].device_id;
|
||||
|
||||
// Create AUGraph
|
||||
if (NewAUGraph(&engine->graph) != noErr) return false;
|
||||
|
||||
// Add HAL output node
|
||||
AudioComponentDescription output_desc = {};
|
||||
output_desc.componentType = kAudioUnitType_Output;
|
||||
output_desc.componentSubType = kAudioUnitSubType_HALOutput;
|
||||
output_desc.componentManufacturer = kAudioUnitManufacturer_Apple;
|
||||
|
||||
AUNode output_node;
|
||||
if (AUGraphAddNode(engine->graph, &output_desc, &output_node) != noErr) {
|
||||
DisposeAUGraph(engine->graph);
|
||||
engine->graph = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AUGraphOpen(engine->graph) != noErr) {
|
||||
DisposeAUGraph(engine->graph);
|
||||
engine->graph = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AUGraphNodeInfo(engine->graph, output_node, nullptr, &engine->output_unit) != noErr) {
|
||||
DisposeAUGraph(engine->graph);
|
||||
engine->graph = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set device
|
||||
if (AudioUnitSetProperty(engine->output_unit, kAudioOutputUnitProperty_CurrentDevice,
|
||||
kAudioUnitScope_Global, 0, &device_id, sizeof(device_id)) != noErr) {
|
||||
DisposeAUGraph(engine->graph);
|
||||
engine->graph = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get device sample rate
|
||||
AudioObjectPropertyAddress rate_prop = {
|
||||
kAudioDevicePropertyNominalSampleRate,
|
||||
kAudioDevicePropertyScopeOutput,
|
||||
kAudioObjectPropertyElementMain
|
||||
};
|
||||
Float64 sample_rate = 44100.0;
|
||||
UInt32 rate_size = sizeof(sample_rate);
|
||||
AudioObjectGetPropertyData(device_id, &rate_prop, 0, nullptr, &rate_size, &sample_rate);
|
||||
engine->sample_rate = sample_rate;
|
||||
|
||||
// Set stream format: Float32, non-interleaved
|
||||
AudioStreamBasicDescription fmt = {};
|
||||
fmt.mSampleRate = sample_rate;
|
||||
fmt.mFormatID = kAudioFormatLinearPCM;
|
||||
fmt.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsNonInterleaved | kAudioFormatFlagIsPacked;
|
||||
fmt.mBitsPerChannel = 32;
|
||||
fmt.mChannelsPerFrame = 2;
|
||||
fmt.mFramesPerPacket = 1;
|
||||
fmt.mBytesPerFrame = 4;
|
||||
fmt.mBytesPerPacket = 4;
|
||||
engine->num_channels = 2;
|
||||
|
||||
AudioUnitSetProperty(engine->output_unit, kAudioUnitProperty_StreamFormat,
|
||||
kAudioUnitScope_Input, 0, &fmt, sizeof(fmt));
|
||||
|
||||
// Set render callback
|
||||
AURenderCallbackStruct cb = {};
|
||||
cb.inputProc = audio_render_callback;
|
||||
cb.inputProcRefCon = engine;
|
||||
|
||||
if (AudioUnitSetProperty(engine->output_unit, kAudioUnitProperty_SetRenderCallback,
|
||||
kAudioUnitScope_Input, 0, &cb, sizeof(cb)) != noErr) {
|
||||
DisposeAUGraph(engine->graph);
|
||||
engine->graph = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize and start
|
||||
if (AUGraphInitialize(engine->graph) != noErr) {
|
||||
DisposeAUGraph(engine->graph);
|
||||
engine->graph = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AUGraphStart(engine->graph) != noErr) {
|
||||
DisposeAUGraph(engine->graph);
|
||||
engine->graph = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
engine->active_device_index = index;
|
||||
engine->test_tone_phase = 0.0;
|
||||
atomic_store(&engine->test_tone_active, 0);
|
||||
atomic_store(&engine->test_tone_samples_remaining, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void audio_close_device(AudioEngine *engine) {
|
||||
if (!engine->graph) return;
|
||||
|
||||
atomic_store(&engine->test_tone_active, 0);
|
||||
atomic_store(&engine->test_tone_samples_remaining, 0);
|
||||
|
||||
AUGraphStop(engine->graph);
|
||||
AUGraphUninitialize(engine->graph);
|
||||
DisposeAUGraph(engine->graph);
|
||||
engine->graph = nullptr;
|
||||
engine->output_unit = nullptr;
|
||||
engine->active_device_index = -1;
|
||||
engine->test_tone_phase = 0.0;
|
||||
}
|
||||
|
||||
void audio_play_test_tone(AudioEngine *engine) {
|
||||
if (!engine->graph) return;
|
||||
|
||||
engine->test_tone_phase = 0.0;
|
||||
int32_t total_samples = (int32_t)(engine->sample_rate * AUDIO_TEST_TONE_SEC);
|
||||
atomic_store(&engine->test_tone_samples_remaining, total_samples);
|
||||
atomic_store(&engine->test_tone_active, 1);
|
||||
}
|
||||
|
||||
bool audio_is_test_tone_playing(AudioEngine *engine) {
|
||||
return atomic_load(&engine->test_tone_active) != 0;
|
||||
}
|
||||
|
||||
void audio_update(AudioEngine *engine, float dt) {
|
||||
(void)engine;
|
||||
(void)dt;
|
||||
}
|
||||
@@ -11,8 +11,10 @@
|
||||
////////////////////////////////
|
||||
// Codebase keywords
|
||||
|
||||
#ifndef __APPLE__
|
||||
#define internal static
|
||||
#define global static
|
||||
#endif
|
||||
#define local_persist static
|
||||
|
||||
////////////////////////////////
|
||||
|
||||
28
src/main.cpp
28
src/main.cpp
@@ -1,5 +1,8 @@
|
||||
// Unity build - include all src files here
|
||||
// -mta
|
||||
#ifdef __APPLE__
|
||||
#include <mach/mach_time.h>
|
||||
#endif
|
||||
// [h]
|
||||
#include "base/base_inc.h"
|
||||
#include "platform/platform.h"
|
||||
@@ -14,10 +17,17 @@
|
||||
#include "base/base_inc.cpp"
|
||||
#include "ui/ui_core.cpp"
|
||||
#include "ui/ui_widgets.cpp"
|
||||
#ifdef __APPLE__
|
||||
#include "platform/platform_macos.mm"
|
||||
#include "renderer/renderer_metal.mm"
|
||||
#include "midi/midi_coremidi.cpp"
|
||||
#include "audio/audio_coreaudio.cpp"
|
||||
#else
|
||||
#include "platform/platform_win32.cpp"
|
||||
#include "renderer/renderer_dx12.cpp"
|
||||
#include "midi/midi_win32.cpp"
|
||||
#include "audio/audio_asio.cpp"
|
||||
#endif
|
||||
#include "menus.cpp"
|
||||
|
||||
////////////////////////////////
|
||||
@@ -58,8 +68,14 @@ struct AppState {
|
||||
B32 show_props;
|
||||
B32 show_log;
|
||||
B32 show_midi_devices;
|
||||
#ifdef __APPLE__
|
||||
uint64_t freq_numer;
|
||||
uint64_t freq_denom;
|
||||
uint64_t last_time;
|
||||
#else
|
||||
LARGE_INTEGER freq;
|
||||
LARGE_INTEGER last_time;
|
||||
#endif
|
||||
|
||||
// Tab state
|
||||
S32 right_panel_tab; // 0 = Properties, 1 = MIDI Devices
|
||||
@@ -643,10 +659,16 @@ static void build_ui(AppState *app) {
|
||||
|
||||
static void do_frame(AppState *app) {
|
||||
// Timing
|
||||
#ifdef __APPLE__
|
||||
uint64_t now = mach_absolute_time();
|
||||
F32 dt = (F32)(now - app->last_time) * (F32)app->freq_numer / ((F32)app->freq_denom * 1e9f);
|
||||
app->last_time = now;
|
||||
#else
|
||||
LARGE_INTEGER now;
|
||||
QueryPerformanceCounter(&now);
|
||||
F32 dt = (F32)(now.QuadPart - app->last_time.QuadPart) / (F32)app->freq.QuadPart;
|
||||
app->last_time = now;
|
||||
#endif
|
||||
if (dt > 0.1f) dt = 0.1f;
|
||||
|
||||
// Resize
|
||||
@@ -737,9 +759,15 @@ int main(int argc, char **argv) {
|
||||
app.show_midi_devices = 1;
|
||||
app.demo_dropdown_sel = 1; // default to 48000 Hz
|
||||
snprintf(app.demo_text_a, sizeof(app.demo_text_a), "My Instrument");
|
||||
#ifdef __APPLE__
|
||||
snprintf(app.demo_text_b, sizeof(app.demo_text_b), "~/Samples/output");
|
||||
{ mach_timebase_info_data_t tbi; mach_timebase_info(&tbi); app.freq_numer = tbi.numer; app.freq_denom = tbi.denom; }
|
||||
app.last_time = mach_absolute_time();
|
||||
#else
|
||||
snprintf(app.demo_text_b, sizeof(app.demo_text_b), "C:\\Samples\\output");
|
||||
QueryPerformanceFrequency(&app.freq);
|
||||
QueryPerformanceCounter(&app.last_time);
|
||||
#endif
|
||||
|
||||
platform_set_frame_callback(window, frame_callback, &app);
|
||||
|
||||
|
||||
249
src/midi/midi_coremidi.cpp
Normal file
249
src/midi/midi_coremidi.cpp
Normal file
@@ -0,0 +1,249 @@
|
||||
#include "midi/midi.h"
|
||||
#include <CoreMIDI/CoreMIDI.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <string.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#define MIDI_MAX_DEVICES 64
|
||||
#define MIDI_RELEASE_FLASH_DURATION 0.15f
|
||||
|
||||
struct MidiEngine {
|
||||
MidiDeviceInfo devices[MIDI_MAX_DEVICES];
|
||||
int32_t device_count;
|
||||
|
||||
MIDIClientRef client;
|
||||
MIDIPortRef input_port;
|
||||
|
||||
// Map: source endpoint index -> our device array index
|
||||
int32_t source_to_device[MIDI_MAX_DEVICES];
|
||||
|
||||
// Set atomically from callback thread
|
||||
_Atomic int32_t pending_note_on_vel[MIDI_MAX_DEVICES];
|
||||
_Atomic int32_t pending_note_num[MIDI_MAX_DEVICES];
|
||||
_Atomic int32_t pending_note_off[MIDI_MAX_DEVICES];
|
||||
_Atomic int32_t held_note_count[MIDI_MAX_DEVICES];
|
||||
|
||||
// Main thread only
|
||||
int32_t display_velocity[MIDI_MAX_DEVICES];
|
||||
int32_t display_note[MIDI_MAX_DEVICES];
|
||||
float release_timers[MIDI_MAX_DEVICES];
|
||||
};
|
||||
|
||||
////////////////////////////////
|
||||
// MIDI read callback
|
||||
|
||||
static void midi_read_callback(const MIDIPacketList *pktlist, void *readProcRefCon, void *srcConnRefCon) {
|
||||
(void)readProcRefCon;
|
||||
|
||||
MidiEngine *engine = (MidiEngine *)readProcRefCon;
|
||||
int32_t device_idx = (int32_t)(intptr_t)srcConnRefCon;
|
||||
if (!engine || device_idx < 0 || device_idx >= MIDI_MAX_DEVICES) return;
|
||||
|
||||
const MIDIPacket *packet = &pktlist->packet[0];
|
||||
for (UInt32 i = 0; i < pktlist->numPackets; i++) {
|
||||
// Parse MIDI bytes
|
||||
for (UInt16 j = 0; j < packet->length; ) {
|
||||
uint8_t status = packet->data[j];
|
||||
|
||||
// Skip non-status bytes (running status not handled for simplicity)
|
||||
if (status < 0x80) { j++; continue; }
|
||||
|
||||
uint8_t kind = status & 0xF0;
|
||||
|
||||
if (kind == 0x90 && j + 2 < packet->length) {
|
||||
uint8_t note = packet->data[j + 1];
|
||||
uint8_t velocity = packet->data[j + 2];
|
||||
j += 3;
|
||||
|
||||
if (velocity > 0) {
|
||||
atomic_store(&engine->pending_note_on_vel[device_idx], (int32_t)velocity);
|
||||
atomic_store(&engine->pending_note_num[device_idx], (int32_t)(note + 1));
|
||||
atomic_fetch_add(&engine->held_note_count[device_idx], 1);
|
||||
} else {
|
||||
// Note-on with velocity 0 = note-off
|
||||
atomic_store(&engine->pending_note_off[device_idx], 1);
|
||||
int32_t count = atomic_fetch_sub(&engine->held_note_count[device_idx], 1);
|
||||
if (count <= 1) atomic_store(&engine->held_note_count[device_idx], 0);
|
||||
}
|
||||
} else if (kind == 0x80 && j + 2 < packet->length) {
|
||||
j += 3;
|
||||
atomic_store(&engine->pending_note_off[device_idx], 1);
|
||||
int32_t count = atomic_fetch_sub(&engine->held_note_count[device_idx], 1);
|
||||
if (count <= 1) atomic_store(&engine->held_note_count[device_idx], 0);
|
||||
} else if (kind == 0xC0 || kind == 0xD0) {
|
||||
j += 2; // Program Change, Channel Pressure (2 bytes)
|
||||
} else if (kind == 0xF0) {
|
||||
// System messages — skip to end or next status byte
|
||||
j++;
|
||||
while (j < packet->length && packet->data[j] < 0x80) j++;
|
||||
} else {
|
||||
j += 3; // Default: 3-byte message
|
||||
}
|
||||
}
|
||||
|
||||
packet = MIDIPacketNext(packet);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Device enumeration
|
||||
|
||||
static void enumerate_midi_devices(MidiEngine *engine) {
|
||||
engine->device_count = 0;
|
||||
|
||||
// Input sources
|
||||
ItemCount num_sources = MIDIGetNumberOfSources();
|
||||
for (ItemCount i = 0; i < num_sources && engine->device_count < MIDI_MAX_DEVICES; i++) {
|
||||
MIDIEndpointRef endpoint = MIDIGetSource((ItemCount)i);
|
||||
|
||||
CFStringRef name_ref = nullptr;
|
||||
MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &name_ref);
|
||||
|
||||
MidiDeviceInfo *dev = &engine->devices[engine->device_count];
|
||||
memset(dev, 0, sizeof(*dev));
|
||||
|
||||
if (name_ref) {
|
||||
CFStringGetCString(name_ref, dev->name, sizeof(dev->name), kCFStringEncodingUTF8);
|
||||
CFRelease(name_ref);
|
||||
} else {
|
||||
snprintf(dev->name, sizeof(dev->name), "MIDI Source %d", (int)i);
|
||||
}
|
||||
|
||||
dev->id = engine->device_count;
|
||||
dev->is_input = true;
|
||||
dev->active = false;
|
||||
|
||||
engine->source_to_device[i] = engine->device_count;
|
||||
engine->device_count++;
|
||||
}
|
||||
|
||||
// Output destinations
|
||||
ItemCount num_dests = MIDIGetNumberOfDestinations();
|
||||
for (ItemCount i = 0; i < num_dests && engine->device_count < MIDI_MAX_DEVICES; i++) {
|
||||
MIDIEndpointRef endpoint = MIDIGetDestination((ItemCount)i);
|
||||
|
||||
CFStringRef name_ref = nullptr;
|
||||
MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &name_ref);
|
||||
|
||||
MidiDeviceInfo *dev = &engine->devices[engine->device_count];
|
||||
memset(dev, 0, sizeof(*dev));
|
||||
|
||||
if (name_ref) {
|
||||
CFStringGetCString(name_ref, dev->name, sizeof(dev->name), kCFStringEncodingUTF8);
|
||||
CFRelease(name_ref);
|
||||
} else {
|
||||
snprintf(dev->name, sizeof(dev->name), "MIDI Dest %d", (int)i);
|
||||
}
|
||||
|
||||
dev->id = engine->device_count;
|
||||
dev->is_input = false;
|
||||
dev->active = false;
|
||||
|
||||
engine->device_count++;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Public API
|
||||
|
||||
MidiEngine *midi_create() {
|
||||
MidiEngine *engine = new MidiEngine();
|
||||
memset(engine, 0, sizeof(*engine));
|
||||
|
||||
MIDIClientCreate(CFSTR("autosample"), nullptr, nullptr, &engine->client);
|
||||
MIDIInputPortCreate(engine->client, CFSTR("Input"), midi_read_callback, engine, &engine->input_port);
|
||||
|
||||
midi_refresh_devices(engine);
|
||||
return engine;
|
||||
}
|
||||
|
||||
void midi_destroy(MidiEngine *engine) {
|
||||
midi_close_all_inputs(engine);
|
||||
if (engine->input_port) MIDIPortDispose(engine->input_port);
|
||||
if (engine->client) MIDIClientDispose(engine->client);
|
||||
delete engine;
|
||||
}
|
||||
|
||||
void midi_open_all_inputs(MidiEngine *engine) {
|
||||
ItemCount num_sources = MIDIGetNumberOfSources();
|
||||
for (ItemCount i = 0; i < num_sources && (int32_t)i < MIDI_MAX_DEVICES; i++) {
|
||||
MIDIEndpointRef endpoint = MIDIGetSource(i);
|
||||
int32_t dev_idx = engine->source_to_device[i];
|
||||
MIDIPortConnectSource(engine->input_port, endpoint, (void *)(intptr_t)dev_idx);
|
||||
}
|
||||
}
|
||||
|
||||
void midi_close_all_inputs(MidiEngine *engine) {
|
||||
ItemCount num_sources = MIDIGetNumberOfSources();
|
||||
for (ItemCount i = 0; i < num_sources; i++) {
|
||||
MIDIEndpointRef endpoint = MIDIGetSource(i);
|
||||
MIDIPortDisconnectSource(engine->input_port, endpoint);
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < MIDI_MAX_DEVICES; i++) {
|
||||
atomic_store(&engine->pending_note_on_vel[i], 0);
|
||||
atomic_store(&engine->pending_note_num[i], 0);
|
||||
atomic_store(&engine->pending_note_off[i], 0);
|
||||
atomic_store(&engine->held_note_count[i], 0);
|
||||
engine->display_velocity[i] = 0;
|
||||
engine->display_note[i] = 0;
|
||||
engine->release_timers[i] = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void midi_update(MidiEngine *engine, float dt) {
|
||||
for (int32_t i = 0; i < engine->device_count; i++) {
|
||||
if (!engine->devices[i].is_input) continue;
|
||||
|
||||
int32_t vel = atomic_exchange(&engine->pending_note_on_vel[i], 0);
|
||||
int32_t note_p1 = atomic_exchange(&engine->pending_note_num[i], 0);
|
||||
if (vel > 0) engine->display_velocity[i] = vel;
|
||||
if (note_p1 > 0) engine->display_note[i] = note_p1 - 1;
|
||||
|
||||
int32_t off = atomic_exchange(&engine->pending_note_off[i], 0);
|
||||
int32_t held = atomic_load(&engine->held_note_count[i]);
|
||||
|
||||
if (held > 0) {
|
||||
engine->devices[i].active = true;
|
||||
engine->devices[i].releasing = false;
|
||||
engine->release_timers[i] = 0.0f;
|
||||
} else if (off || (engine->devices[i].active && held <= 0)) {
|
||||
engine->devices[i].active = false;
|
||||
engine->devices[i].releasing = true;
|
||||
engine->release_timers[i] = MIDI_RELEASE_FLASH_DURATION;
|
||||
}
|
||||
|
||||
if (engine->release_timers[i] > 0.0f) {
|
||||
engine->release_timers[i] -= dt;
|
||||
if (engine->release_timers[i] <= 0.0f) {
|
||||
engine->release_timers[i] = 0.0f;
|
||||
engine->devices[i].releasing = false;
|
||||
engine->display_velocity[i] = 0;
|
||||
engine->display_note[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
engine->devices[i].velocity = engine->display_velocity[i];
|
||||
engine->devices[i].note = engine->display_note[i];
|
||||
}
|
||||
}
|
||||
|
||||
bool midi_is_input_active(MidiEngine *engine, int32_t device_index) {
|
||||
if (device_index < 0 || device_index >= engine->device_count) return false;
|
||||
return engine->devices[device_index].active;
|
||||
}
|
||||
|
||||
void midi_refresh_devices(MidiEngine *engine) {
|
||||
midi_close_all_inputs(engine);
|
||||
enumerate_midi_devices(engine);
|
||||
midi_open_all_inputs(engine);
|
||||
}
|
||||
|
||||
int32_t midi_get_device_count(MidiEngine *engine) {
|
||||
return engine->device_count;
|
||||
}
|
||||
|
||||
MidiDeviceInfo *midi_get_device(MidiEngine *engine, int32_t index) {
|
||||
if (index < 0 || index >= engine->device_count) return nullptr;
|
||||
return &engine->devices[index];
|
||||
}
|
||||
426
src/platform/platform_macos.mm
Normal file
426
src/platform/platform_macos.mm
Normal file
@@ -0,0 +1,426 @@
|
||||
#include "platform/platform.h"
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
// macOS virtual key codes (avoids Carbon.h include)
|
||||
enum {
|
||||
kVK_ANSI_A = 0x00, kVK_ANSI_C = 0x08, kVK_ANSI_V = 0x09,
|
||||
kVK_ANSI_X = 0x07,
|
||||
kVK_Return = 0x24, kVK_Tab = 0x30, kVK_Delete = 0x33,
|
||||
kVK_Escape = 0x35, kVK_ForwardDelete = 0x75,
|
||||
kVK_LeftArrow = 0x7B, kVK_RightArrow = 0x7C,
|
||||
kVK_DownArrow = 0x7D, kVK_UpArrow = 0x7E,
|
||||
kVK_Home = 0x73, kVK_End = 0x77,
|
||||
kVK_Command = 0x37, kVK_Shift = 0x38,
|
||||
kVK_RightShift = 0x3C, kVK_RightCommand = 0x36,
|
||||
};
|
||||
|
||||
static uint8_t macos_keycode_to_pkey(uint16_t keycode) {
|
||||
switch (keycode) {
|
||||
case kVK_ANSI_A: return PKEY_A;
|
||||
case kVK_ANSI_C: return PKEY_C;
|
||||
case kVK_ANSI_V: return PKEY_V;
|
||||
case kVK_ANSI_X: return PKEY_X;
|
||||
case kVK_Return: return PKEY_RETURN;
|
||||
case kVK_Tab: return PKEY_TAB;
|
||||
case kVK_Delete: return PKEY_BACKSPACE;
|
||||
case kVK_ForwardDelete:return PKEY_DELETE;
|
||||
case kVK_Escape: return PKEY_ESCAPE;
|
||||
case kVK_LeftArrow: return PKEY_LEFT;
|
||||
case kVK_RightArrow: return PKEY_RIGHT;
|
||||
case kVK_UpArrow: return PKEY_UP;
|
||||
case kVK_DownArrow: return PKEY_DOWN;
|
||||
case kVK_Home: return PKEY_HOME;
|
||||
case kVK_End: return PKEY_END;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Forward declarations
|
||||
|
||||
struct PlatformWindow;
|
||||
static PlatformWindow *g_current_window = nullptr;
|
||||
|
||||
////////////////////////////////
|
||||
// Objective-C helper classes
|
||||
|
||||
@interface ASmplWindowDelegate : NSObject <NSWindowDelegate>
|
||||
@end
|
||||
|
||||
@implementation ASmplWindowDelegate
|
||||
- (BOOL)windowShouldClose:(id)sender {
|
||||
(void)sender;
|
||||
if (g_current_window) {
|
||||
// Set should_close flag (accessed via the struct below)
|
||||
extern void platform_macos_set_should_close();
|
||||
platform_macos_set_should_close();
|
||||
}
|
||||
return NO; // We handle closing ourselves
|
||||
}
|
||||
|
||||
- (void)windowDidResize:(NSNotification *)notification {
|
||||
(void)notification;
|
||||
if (!g_current_window) return;
|
||||
extern void platform_macos_handle_resize();
|
||||
platform_macos_handle_resize();
|
||||
}
|
||||
@end
|
||||
|
||||
@interface ASmplView : NSView <NSTextInputClient>
|
||||
@end
|
||||
|
||||
@implementation ASmplView
|
||||
|
||||
- (BOOL)acceptsFirstResponder { return YES; }
|
||||
- (BOOL)canBecomeKeyView { return YES; }
|
||||
|
||||
// Needed for NSTextInputClient
|
||||
- (BOOL)hasMarkedText { return NO; }
|
||||
- (NSRange)markedRange { return NSMakeRange(NSNotFound, 0); }
|
||||
- (NSRange)selectedRange { return NSMakeRange(NSNotFound, 0); }
|
||||
- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange {
|
||||
(void)string; (void)selectedRange; (void)replacementRange;
|
||||
}
|
||||
- (void)unmarkText {}
|
||||
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText { return @[]; }
|
||||
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
|
||||
(void)range; (void)actualRange;
|
||||
return nil;
|
||||
}
|
||||
- (NSUInteger)characterIndexForPoint:(NSPoint)point { (void)point; return NSNotFound; }
|
||||
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
|
||||
(void)range; (void)actualRange;
|
||||
return NSZeroRect;
|
||||
}
|
||||
|
||||
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange {
|
||||
(void)replacementRange;
|
||||
NSString *str = nil;
|
||||
if ([string isKindOfClass:[NSAttributedString class]])
|
||||
str = [string string];
|
||||
else
|
||||
str = (NSString *)string;
|
||||
|
||||
extern void platform_macos_insert_text(const char *utf8);
|
||||
platform_macos_insert_text([str UTF8String]);
|
||||
}
|
||||
|
||||
- (void)keyDown:(NSEvent *)event {
|
||||
extern void platform_macos_key_down(uint16_t keycode, NSEventModifierFlags mods);
|
||||
platform_macos_key_down([event keyCode], [event modifierFlags]);
|
||||
|
||||
// Feed into text input system for character generation
|
||||
[self interpretKeyEvents:@[event]];
|
||||
}
|
||||
|
||||
- (void)flagsChanged:(NSEvent *)event {
|
||||
(void)event;
|
||||
// Modifiers are read at key-down time, nothing to accumulate here
|
||||
}
|
||||
|
||||
- (void)mouseDown:(NSEvent *)event {
|
||||
(void)event;
|
||||
extern void platform_macos_mouse_down();
|
||||
platform_macos_mouse_down();
|
||||
}
|
||||
|
||||
- (void)mouseUp:(NSEvent *)event {
|
||||
(void)event;
|
||||
extern void platform_macos_mouse_up();
|
||||
platform_macos_mouse_up();
|
||||
}
|
||||
|
||||
- (void)mouseMoved:(NSEvent *)event { (void)event; }
|
||||
- (void)mouseDragged:(NSEvent *)event { (void)event; }
|
||||
|
||||
- (void)scrollWheel:(NSEvent *)event {
|
||||
extern void platform_macos_scroll(float dx, float dy);
|
||||
float dy = (float)[event scrollingDeltaY];
|
||||
if ([event hasPreciseScrollingDeltas])
|
||||
dy /= 40.0f; // Normalize trackpad deltas to match discrete wheel steps
|
||||
platform_macos_scroll(0, dy);
|
||||
}
|
||||
|
||||
- (BOOL)acceptsFirstMouse:(NSEvent *)event { (void)event; return YES; }
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////
|
||||
// PlatformWindow struct
|
||||
|
||||
struct PlatformWindow {
|
||||
NSWindow *ns_window;
|
||||
ASmplView *view;
|
||||
ASmplWindowDelegate *delegate;
|
||||
bool should_close;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
int32_t pending_menu_cmd;
|
||||
PlatformFrameCallback frame_callback;
|
||||
void *frame_callback_user_data;
|
||||
PlatformInput input;
|
||||
B32 prev_mouse_down;
|
||||
B32 mouse_down_state;
|
||||
F32 backing_scale;
|
||||
};
|
||||
|
||||
////////////////////////////////
|
||||
// C callback helpers (called from ObjC)
|
||||
|
||||
void platform_macos_set_should_close() {
|
||||
if (g_current_window) g_current_window->should_close = true;
|
||||
}
|
||||
|
||||
void platform_macos_handle_resize() {
|
||||
if (!g_current_window) return;
|
||||
NSRect frame = [g_current_window->view bounds];
|
||||
F32 scale = g_current_window->backing_scale;
|
||||
g_current_window->width = (int32_t)(frame.size.width * scale);
|
||||
g_current_window->height = (int32_t)(frame.size.height * scale);
|
||||
if (g_current_window->frame_callback)
|
||||
g_current_window->frame_callback(g_current_window->frame_callback_user_data);
|
||||
}
|
||||
|
||||
void platform_macos_insert_text(const char *utf8) {
|
||||
if (!g_current_window || !utf8) return;
|
||||
PlatformInput *ev = &g_current_window->input;
|
||||
while (*utf8 && ev->char_count < PLATFORM_MAX_CHARS_PER_FRAME) {
|
||||
uint8_t c = (uint8_t)*utf8;
|
||||
if (c < 32) { utf8++; continue; }
|
||||
// Handle ASCII printable range (single-byte UTF-8)
|
||||
if (c < 0x80) {
|
||||
ev->chars[ev->char_count++] = (uint16_t)c;
|
||||
utf8++;
|
||||
} else {
|
||||
// Skip multi-byte UTF-8 sequences for now (UI only handles ASCII)
|
||||
if (c < 0xE0) utf8 += 2;
|
||||
else if (c < 0xF0) utf8 += 3;
|
||||
else utf8 += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void platform_macos_key_down(uint16_t keycode, NSEventModifierFlags mods) {
|
||||
if (!g_current_window) return;
|
||||
PlatformInput *ev = &g_current_window->input;
|
||||
|
||||
uint8_t pkey = macos_keycode_to_pkey(keycode);
|
||||
if (pkey && ev->key_count < PLATFORM_MAX_KEYS_PER_FRAME)
|
||||
ev->keys[ev->key_count++] = pkey;
|
||||
|
||||
// Command = ctrl_held (macOS convention: Cmd+C, Cmd+V, etc.)
|
||||
ev->ctrl_held = (mods & NSEventModifierFlagCommand) != 0;
|
||||
ev->shift_held = (mods & NSEventModifierFlagShift) != 0;
|
||||
}
|
||||
|
||||
void platform_macos_mouse_down() {
|
||||
if (g_current_window) g_current_window->mouse_down_state = 1;
|
||||
}
|
||||
|
||||
void platform_macos_mouse_up() {
|
||||
if (g_current_window) g_current_window->mouse_down_state = 0;
|
||||
}
|
||||
|
||||
void platform_macos_scroll(float dx, float dy) {
|
||||
(void)dx;
|
||||
if (g_current_window) g_current_window->input.scroll_delta.y += dy;
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Menu action handler
|
||||
|
||||
@interface ASmplMenuTarget : NSObject
|
||||
- (void)menuAction:(id)sender;
|
||||
@end
|
||||
|
||||
@implementation ASmplMenuTarget
|
||||
- (void)menuAction:(id)sender {
|
||||
if (!g_current_window) return;
|
||||
NSMenuItem *item = (NSMenuItem *)sender;
|
||||
g_current_window->pending_menu_cmd = (int32_t)[item tag];
|
||||
}
|
||||
@end
|
||||
|
||||
static ASmplMenuTarget *g_menu_target = nil;
|
||||
|
||||
////////////////////////////////
|
||||
// Public API
|
||||
|
||||
PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
|
||||
// Ensure NSApplication is initialized
|
||||
[NSApplication sharedApplication];
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
|
||||
NSRect content_rect = NSMakeRect(0, 0, desc->width, desc->height);
|
||||
|
||||
NSWindow *ns_window = [[NSWindow alloc]
|
||||
initWithContentRect:content_rect
|
||||
styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
|
||||
NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable)
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
|
||||
[ns_window setTitle:[NSString stringWithUTF8String:desc->title]];
|
||||
[ns_window center];
|
||||
|
||||
ASmplView *view = [[ASmplView alloc] initWithFrame:content_rect];
|
||||
[ns_window setContentView:view];
|
||||
[ns_window makeFirstResponder:view];
|
||||
|
||||
// Enable mouse moved events
|
||||
[ns_window setAcceptsMouseMovedEvents:YES];
|
||||
|
||||
ASmplWindowDelegate *delegate = [[ASmplWindowDelegate alloc] init];
|
||||
[ns_window setDelegate:delegate];
|
||||
|
||||
PlatformWindow *window = new PlatformWindow();
|
||||
memset(window, 0, sizeof(*window));
|
||||
window->ns_window = ns_window;
|
||||
window->view = view;
|
||||
window->delegate = delegate;
|
||||
window->should_close = false;
|
||||
window->backing_scale = (F32)[ns_window backingScaleFactor];
|
||||
window->width = (int32_t)(desc->width * window->backing_scale);
|
||||
window->height = (int32_t)(desc->height * window->backing_scale);
|
||||
|
||||
g_current_window = window;
|
||||
|
||||
[ns_window makeKeyAndOrderFront:nil];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
void platform_destroy_window(PlatformWindow *window) {
|
||||
if (!window) return;
|
||||
|
||||
[window->ns_window close];
|
||||
if (g_current_window == window)
|
||||
g_current_window = nullptr;
|
||||
delete window;
|
||||
}
|
||||
|
||||
bool platform_poll_events(PlatformWindow *window) {
|
||||
@autoreleasepool {
|
||||
NSEvent *event;
|
||||
while ((event = [NSApp nextEventMatchingMask:NSEventMaskAny
|
||||
untilDate:nil
|
||||
inMode:NSDefaultRunLoopMode
|
||||
dequeue:YES])) {
|
||||
[NSApp sendEvent:event];
|
||||
}
|
||||
}
|
||||
return !window->should_close;
|
||||
}
|
||||
|
||||
void platform_get_size(PlatformWindow *window, int32_t *w, int32_t *h) {
|
||||
if (w) *w = window->width;
|
||||
if (h) *h = window->height;
|
||||
}
|
||||
|
||||
void *platform_get_native_handle(PlatformWindow *window) {
|
||||
return (__bridge void *)window->view;
|
||||
}
|
||||
|
||||
void platform_set_frame_callback(PlatformWindow *window, PlatformFrameCallback cb, void *user_data) {
|
||||
window->frame_callback = cb;
|
||||
window->frame_callback_user_data = user_data;
|
||||
}
|
||||
|
||||
void platform_set_menu(PlatformWindow *window, PlatformMenu *menus, int32_t menu_count) {
|
||||
(void)window;
|
||||
|
||||
if (!g_menu_target)
|
||||
g_menu_target = [[ASmplMenuTarget alloc] init];
|
||||
|
||||
NSMenu *menu_bar = [[NSMenu alloc] init];
|
||||
|
||||
// App menu (required on macOS)
|
||||
NSMenuItem *app_menu_item = [[NSMenuItem alloc] init];
|
||||
NSMenu *app_menu = [[NSMenu alloc] init];
|
||||
[app_menu addItemWithTitle:@"Quit autosample"
|
||||
action:@selector(terminate:)
|
||||
keyEquivalent:@"q"];
|
||||
[app_menu_item setSubmenu:app_menu];
|
||||
[menu_bar addItem:app_menu_item];
|
||||
|
||||
for (int32_t i = 0; i < menu_count; i++) {
|
||||
NSMenuItem *top_item = [[NSMenuItem alloc] init];
|
||||
NSMenu *submenu = [[NSMenu alloc] initWithTitle:
|
||||
[NSString stringWithUTF8String:menus[i].label]];
|
||||
|
||||
for (int32_t j = 0; j < menus[i].item_count; j++) {
|
||||
PlatformMenuItem *item = &menus[i].items[j];
|
||||
if (!item->label) {
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
} else {
|
||||
NSMenuItem *ns_item = [[NSMenuItem alloc]
|
||||
initWithTitle:[NSString stringWithUTF8String:item->label]
|
||||
action:@selector(menuAction:)
|
||||
keyEquivalent:@""];
|
||||
[ns_item setTag:item->id];
|
||||
[ns_item setTarget:g_menu_target];
|
||||
[submenu addItem:ns_item];
|
||||
}
|
||||
}
|
||||
|
||||
[top_item setSubmenu:submenu];
|
||||
[menu_bar addItem:top_item];
|
||||
}
|
||||
|
||||
[NSApp setMainMenu:menu_bar];
|
||||
}
|
||||
|
||||
int32_t platform_poll_menu_command(PlatformWindow *window) {
|
||||
int32_t cmd = window->pending_menu_cmd;
|
||||
window->pending_menu_cmd = 0;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
PlatformInput platform_get_input(PlatformWindow *window) {
|
||||
PlatformInput result = window->input;
|
||||
|
||||
// Poll mouse position (Cocoa uses bottom-left origin, flip Y)
|
||||
NSPoint mouse_in_window = [window->ns_window mouseLocationOutsideOfEventStream];
|
||||
NSRect view_bounds = [window->view bounds];
|
||||
F32 scale = window->backing_scale;
|
||||
result.mouse_pos = v2f32(
|
||||
(F32)mouse_in_window.x * scale,
|
||||
(F32)(view_bounds.size.height - mouse_in_window.y) * scale);
|
||||
|
||||
// Mouse button state
|
||||
result.was_mouse_down = window->prev_mouse_down;
|
||||
result.mouse_down = window->mouse_down_state;
|
||||
window->prev_mouse_down = result.mouse_down;
|
||||
|
||||
// Clear accumulated events for next frame
|
||||
window->input = {};
|
||||
return result;
|
||||
}
|
||||
|
||||
void platform_clipboard_set(const char *text) {
|
||||
if (!text) return;
|
||||
NSPasteboard *pb = [NSPasteboard generalPasteboard];
|
||||
[pb clearContents];
|
||||
[pb setString:[NSString stringWithUTF8String:text] forType:NSPasteboardTypeString];
|
||||
}
|
||||
|
||||
const char *platform_clipboard_get() {
|
||||
static char buf[4096];
|
||||
buf[0] = '\0';
|
||||
|
||||
NSPasteboard *pb = [NSPasteboard generalPasteboard];
|
||||
NSString *str = [pb stringForType:NSPasteboardTypeString];
|
||||
if (str) {
|
||||
const char *utf8 = [str UTF8String];
|
||||
if (utf8) {
|
||||
size_t len = strlen(utf8);
|
||||
if (len >= sizeof(buf)) len = sizeof(buf) - 1;
|
||||
memcpy(buf, utf8, len);
|
||||
buf[len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
return buf[0] ? buf : nullptr;
|
||||
}
|
||||
818
src/renderer/renderer_metal.mm
Normal file
818
src/renderer/renderer_metal.mm
Normal file
@@ -0,0 +1,818 @@
|
||||
#include "renderer/renderer.h"
|
||||
#include "ui/ui_core.h"
|
||||
#include "ui/ui_theme.h"
|
||||
|
||||
#import <Metal/Metal.h>
|
||||
#import <QuartzCore/CAMetalLayer.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <CoreText/CoreText.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#include <math.h>
|
||||
|
||||
#define NUM_BACK_BUFFERS 2
|
||||
#define MAX_VERTICES (64 * 1024)
|
||||
#define MAX_INDICES (MAX_VERTICES * 3)
|
||||
|
||||
// Font atlas
|
||||
#define FONT_ATLAS_W 1024
|
||||
#define FONT_ATLAS_H 1024
|
||||
#define GLYPH_FIRST 32
|
||||
#define GLYPH_LAST 126
|
||||
#define GLYPH_COUNT (GLYPH_LAST - GLYPH_FIRST + 1)
|
||||
|
||||
////////////////////////////////
|
||||
// Vertex format — matches DX12 UIVertex exactly
|
||||
|
||||
struct UIVertex {
|
||||
float pos[2];
|
||||
float uv[2];
|
||||
float col[4];
|
||||
float rect_min[2];
|
||||
float rect_max[2];
|
||||
float corner_radii[4]; // TL, TR, BR, BL
|
||||
float border_thickness;
|
||||
float softness;
|
||||
float mode; // 0 = rect SDF, 1 = textured
|
||||
};
|
||||
|
||||
////////////////////////////////
|
||||
// Glyph info
|
||||
|
||||
struct GlyphInfo {
|
||||
F32 u0, v0, u1, v1;
|
||||
F32 w, h;
|
||||
F32 x_advance;
|
||||
};
|
||||
|
||||
////////////////////////////////
|
||||
// Metal shader (MSL) — port of HLSL SDF shader
|
||||
|
||||
static const char *g_shader_msl = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct Vertex {
|
||||
float2 pos [[attribute(0)]];
|
||||
float2 uv [[attribute(1)]];
|
||||
float4 col [[attribute(2)]];
|
||||
float2 rect_min [[attribute(3)]];
|
||||
float2 rect_max [[attribute(4)]];
|
||||
float4 corner_radii [[attribute(5)]];
|
||||
float border_thickness [[attribute(6)]];
|
||||
float softness [[attribute(7)]];
|
||||
float mode [[attribute(8)]];
|
||||
};
|
||||
|
||||
struct Fragment {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
float4 col;
|
||||
float2 rect_min;
|
||||
float2 rect_max;
|
||||
float4 corner_radii;
|
||||
float border_thickness;
|
||||
float softness;
|
||||
float mode;
|
||||
};
|
||||
|
||||
struct Constants {
|
||||
float2 viewport_size;
|
||||
};
|
||||
|
||||
vertex Fragment vertex_main(Vertex in [[stage_in]],
|
||||
constant Constants &cb [[buffer(1)]]) {
|
||||
Fragment out;
|
||||
float2 ndc;
|
||||
ndc.x = (in.pos.x / cb.viewport_size.x) * 2.0 - 1.0;
|
||||
ndc.y = 1.0 - (in.pos.y / cb.viewport_size.y) * 2.0;
|
||||
out.pos = float4(ndc, 0.0, 1.0);
|
||||
out.uv = in.uv;
|
||||
out.col = in.col;
|
||||
out.rect_min = in.rect_min;
|
||||
out.rect_max = in.rect_max;
|
||||
out.corner_radii = in.corner_radii;
|
||||
out.border_thickness = in.border_thickness;
|
||||
out.softness = in.softness;
|
||||
out.mode = in.mode;
|
||||
return out;
|
||||
}
|
||||
|
||||
float rounded_rect_sdf(float2 sample_pos, float2 rect_center, float2 rect_half_size, float radius) {
|
||||
float2 d = abs(sample_pos - rect_center) - rect_half_size + float2(radius, radius);
|
||||
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius;
|
||||
}
|
||||
|
||||
fragment float4 fragment_main(Fragment in [[stage_in]],
|
||||
texture2d<float> font_tex [[texture(0)]],
|
||||
sampler font_smp [[sampler(0)]]) {
|
||||
float4 col = in.col;
|
||||
|
||||
if (in.mode > 0.5) {
|
||||
float alpha = font_tex.sample(font_smp, in.uv).r;
|
||||
col.a *= alpha;
|
||||
} else {
|
||||
float2 pixel_pos = in.pos.xy;
|
||||
float2 rect_center = (in.rect_min + in.rect_max) * 0.5;
|
||||
float2 rect_half_size = (in.rect_max - in.rect_min) * 0.5;
|
||||
float radius = (pixel_pos.x < rect_center.x)
|
||||
? ((pixel_pos.y < rect_center.y) ? in.corner_radii.x : in.corner_radii.w)
|
||||
: ((pixel_pos.y < rect_center.y) ? in.corner_radii.y : in.corner_radii.z);
|
||||
float softness = max(in.softness, 0.5);
|
||||
float dist = rounded_rect_sdf(pixel_pos, rect_center, rect_half_size, radius);
|
||||
|
||||
if (in.border_thickness > 0) {
|
||||
float inner_dist = dist + in.border_thickness;
|
||||
float outer_alpha = 1.0 - smoothstep(-softness, softness, dist);
|
||||
float inner_alpha = smoothstep(-softness, softness, inner_dist);
|
||||
col.a *= outer_alpha * inner_alpha;
|
||||
} else {
|
||||
col.a *= 1.0 - smoothstep(-softness, softness, dist);
|
||||
}
|
||||
}
|
||||
|
||||
if (col.a < 0.002) discard_fragment();
|
||||
return col;
|
||||
}
|
||||
)";
|
||||
|
||||
////////////////////////////////
|
||||
// Renderer struct
|
||||
|
||||
struct Renderer {
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
int32_t frame_count;
|
||||
uint32_t frame_index;
|
||||
F32 backing_scale;
|
||||
|
||||
id<MTLDevice> device;
|
||||
id<MTLCommandQueue> command_queue;
|
||||
CAMetalLayer *metal_layer;
|
||||
id<MTLRenderPipelineState> pipeline_state;
|
||||
|
||||
dispatch_semaphore_t frame_semaphore;
|
||||
|
||||
// Double-buffered vertex/index buffers
|
||||
id<MTLBuffer> vertex_buffers[NUM_BACK_BUFFERS];
|
||||
id<MTLBuffer> index_buffers[NUM_BACK_BUFFERS];
|
||||
|
||||
// Font atlas
|
||||
id<MTLTexture> font_texture;
|
||||
id<MTLSamplerState> font_sampler;
|
||||
GlyphInfo glyphs[GLYPH_COUNT];
|
||||
F32 font_atlas_size;
|
||||
F32 font_line_height;
|
||||
|
||||
// Text measurement (Core Text)
|
||||
CTFontRef measure_font;
|
||||
F32 measure_font_size;
|
||||
};
|
||||
|
||||
////////////////////////////////
|
||||
// Font atlas (Core Text + CoreGraphics)
|
||||
|
||||
static bool create_font_atlas(Renderer *r, F32 font_size) {
|
||||
const int SS = 2;
|
||||
F32 render_size = font_size * SS;
|
||||
int render_w = FONT_ATLAS_W * SS;
|
||||
int render_h = FONT_ATLAS_H * SS;
|
||||
|
||||
r->font_atlas_size = font_size;
|
||||
|
||||
// Create Core Text font (system font = SF Pro on macOS)
|
||||
CTFontRef font = CTFontCreateUIFontForLanguage(kCTFontUIFontSystem, render_size, nullptr);
|
||||
if (!font) return false;
|
||||
|
||||
// Get line height
|
||||
F32 ascent = (F32)CTFontGetAscent(font);
|
||||
F32 descent = (F32)CTFontGetDescent(font);
|
||||
F32 leading = (F32)CTFontGetLeading(font);
|
||||
r->font_line_height = (ascent + descent + leading) / SS;
|
||||
|
||||
// Create bitmap context at supersampled resolution
|
||||
CGColorSpaceRef color_space = CGColorSpaceCreateDeviceGray();
|
||||
CGContextRef ctx = CGBitmapContextCreate(nullptr, render_w, render_h, 8, render_w,
|
||||
color_space, kCGImageAlphaNone);
|
||||
CGColorSpaceRelease(color_space);
|
||||
if (!ctx) { CFRelease(font); return false; }
|
||||
|
||||
// Clear to black
|
||||
CGContextSetGrayFillColor(ctx, 0.0, 1.0);
|
||||
CGContextFillRect(ctx, CGRectMake(0, 0, render_w, render_h));
|
||||
|
||||
// White text
|
||||
CGContextSetGrayFillColor(ctx, 1.0, 1.0);
|
||||
|
||||
// Render each glyph
|
||||
int pen_x = SS, pen_y = SS;
|
||||
int row_height = 0;
|
||||
|
||||
NSDictionary *attrs = @{
|
||||
(id)kCTFontAttributeName: (__bridge id)font,
|
||||
(id)kCTForegroundColorFromContextAttributeName: @YES
|
||||
};
|
||||
|
||||
for (int i = 0; i < GLYPH_COUNT; i++) {
|
||||
char ch = (char)(GLYPH_FIRST + i);
|
||||
NSString *str = [[NSString alloc] initWithBytes:&ch length:1 encoding:NSASCIIStringEncoding];
|
||||
NSAttributedString *astr = [[NSAttributedString alloc] initWithString:str attributes:attrs];
|
||||
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)astr);
|
||||
|
||||
CGRect bounds = CTLineGetBoundsWithOptions(line, 0);
|
||||
int gw = (int)ceilf((float)bounds.size.width) + 2 * SS;
|
||||
int gh = (int)ceilf((float)(ascent + descent)) + 2 * SS;
|
||||
|
||||
if (pen_x + gw >= render_w) {
|
||||
pen_x = SS;
|
||||
pen_y += row_height + SS;
|
||||
row_height = 0;
|
||||
}
|
||||
|
||||
if (pen_y + gh >= render_h) { CFRelease(line); break; }
|
||||
|
||||
// CoreGraphics has bottom-left origin; draw relative to pen position
|
||||
F32 draw_x = (F32)(pen_x + SS) - (F32)bounds.origin.x;
|
||||
F32 draw_y = (F32)(render_h - pen_y - SS) - ascent;
|
||||
|
||||
CGContextSetTextPosition(ctx, draw_x, draw_y);
|
||||
CTLineDraw(line, ctx);
|
||||
|
||||
// UVs (same fractional math as DX12)
|
||||
r->glyphs[i].u0 = (F32)pen_x / (F32)render_w;
|
||||
r->glyphs[i].v0 = (F32)pen_y / (F32)render_h;
|
||||
r->glyphs[i].u1 = (F32)(pen_x + gw) / (F32)render_w;
|
||||
r->glyphs[i].v1 = (F32)(pen_y + gh) / (F32)render_h;
|
||||
r->glyphs[i].w = (F32)gw / SS;
|
||||
r->glyphs[i].h = (F32)gh / SS;
|
||||
r->glyphs[i].x_advance = (F32)bounds.size.width / SS;
|
||||
|
||||
if (gh > row_height) row_height = gh;
|
||||
pen_x += gw + SS;
|
||||
|
||||
CFRelease(line);
|
||||
}
|
||||
|
||||
// Box-filter downsample
|
||||
uint8_t *src = (uint8_t *)CGBitmapContextGetData(ctx);
|
||||
uint8_t *atlas_data = (uint8_t *)malloc(FONT_ATLAS_W * FONT_ATLAS_H);
|
||||
|
||||
// CoreGraphics bitmap has bottom-left origin — flip Y during downsample
|
||||
for (int y = 0; y < FONT_ATLAS_H; y++) {
|
||||
for (int x = 0; x < FONT_ATLAS_W; x++) {
|
||||
int sum = 0;
|
||||
for (int sy = 0; sy < SS; sy++) {
|
||||
for (int sx = 0; sx < SS; sx++) {
|
||||
int src_y = render_h - 1 - (y * SS + sy); // flip Y
|
||||
int src_idx = src_y * render_w + (x * SS + sx);
|
||||
sum += src[src_idx];
|
||||
}
|
||||
}
|
||||
float a = (float)sum / (float)(SS * SS * 255);
|
||||
a = powf(a, 0.55f);
|
||||
atlas_data[y * FONT_ATLAS_W + x] = (uint8_t)(a * 255.0f + 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
CGContextRelease(ctx);
|
||||
CFRelease(font);
|
||||
|
||||
// Create Metal texture
|
||||
MTLTextureDescriptor *tex_desc = [[MTLTextureDescriptor alloc] init];
|
||||
tex_desc.pixelFormat = MTLPixelFormatR8Unorm;
|
||||
tex_desc.width = FONT_ATLAS_W;
|
||||
tex_desc.height = FONT_ATLAS_H;
|
||||
tex_desc.usage = MTLTextureUsageShaderRead;
|
||||
|
||||
r->font_texture = [r->device newTextureWithDescriptor:tex_desc];
|
||||
[r->font_texture replaceRegion:MTLRegionMake2D(0, 0, FONT_ATLAS_W, FONT_ATLAS_H)
|
||||
mipmapLevel:0
|
||||
withBytes:atlas_data
|
||||
bytesPerRow:FONT_ATLAS_W];
|
||||
|
||||
free(atlas_data);
|
||||
|
||||
// Create sampler
|
||||
MTLSamplerDescriptor *samp_desc = [[MTLSamplerDescriptor alloc] init];
|
||||
samp_desc.minFilter = MTLSamplerMinMagFilterLinear;
|
||||
samp_desc.magFilter = MTLSamplerMinMagFilterLinear;
|
||||
samp_desc.sAddressMode = MTLSamplerAddressModeClampToEdge;
|
||||
samp_desc.tAddressMode = MTLSamplerAddressModeClampToEdge;
|
||||
r->font_sampler = [r->device newSamplerStateWithDescriptor:samp_desc];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Text measurement
|
||||
|
||||
static void ensure_measure_font(Renderer *r, F32 font_size) {
|
||||
if (r->measure_font && r->measure_font_size == font_size) return;
|
||||
if (r->measure_font) CFRelease(r->measure_font);
|
||||
r->measure_font = CTFontCreateUIFontForLanguage(kCTFontUIFontSystem, font_size, nullptr);
|
||||
r->measure_font_size = font_size;
|
||||
}
|
||||
|
||||
Vec2F32 renderer_measure_text(const char *text, int32_t length, float font_size, void *user_data) {
|
||||
Renderer *r = (Renderer *)user_data;
|
||||
if (!r || length == 0) return v2f32(0, font_size);
|
||||
|
||||
ensure_measure_font(r, font_size);
|
||||
|
||||
NSString *str = [[NSString alloc] initWithBytes:text length:length encoding:NSUTF8StringEncoding];
|
||||
if (!str) return v2f32(0, font_size);
|
||||
|
||||
NSDictionary *attrs = @{ (id)kCTFontAttributeName: (__bridge id)r->measure_font };
|
||||
NSAttributedString *astr = [[NSAttributedString alloc] initWithString:str attributes:attrs];
|
||||
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)astr);
|
||||
|
||||
CGRect bounds = CTLineGetBoundsWithOptions(line, 0);
|
||||
CFRelease(line);
|
||||
|
||||
return v2f32((F32)bounds.size.width, (F32)bounds.size.height);
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Draw batch and quad emission (same logic as DX12)
|
||||
|
||||
struct DrawBatch {
|
||||
UIVertex *vertices;
|
||||
U32 *indices;
|
||||
U32 vertex_count;
|
||||
U32 index_count;
|
||||
};
|
||||
|
||||
static void emit_quad(DrawBatch *batch,
|
||||
float x0, float y0, float x1, float y1,
|
||||
float u0, float v0, float u1, float v1,
|
||||
float cr, float cg, float cb, float ca,
|
||||
float rmin_x, float rmin_y, float rmax_x, float rmax_y,
|
||||
float cr_tl, float cr_tr, float cr_br, float cr_bl,
|
||||
float border_thickness, float softness, float mode)
|
||||
{
|
||||
if (batch->vertex_count + 4 > MAX_VERTICES || batch->index_count + 6 > MAX_INDICES)
|
||||
return;
|
||||
|
||||
U32 base = batch->vertex_count;
|
||||
UIVertex *v = &batch->vertices[base];
|
||||
|
||||
float px0 = x0, py0 = y0, px1 = x1, py1 = y1;
|
||||
if (mode < 0.5f) {
|
||||
float pad = softness + 1.0f;
|
||||
px0 -= pad; py0 -= pad; px1 += pad; py1 += pad;
|
||||
}
|
||||
|
||||
v[0].pos[0] = px0; v[0].pos[1] = py0; v[0].uv[0] = u0; v[0].uv[1] = v0;
|
||||
v[1].pos[0] = px1; v[1].pos[1] = py0; v[1].uv[0] = u1; v[1].uv[1] = v0;
|
||||
v[2].pos[0] = px1; v[2].pos[1] = py1; v[2].uv[0] = u1; v[2].uv[1] = v1;
|
||||
v[3].pos[0] = px0; v[3].pos[1] = py1; v[3].uv[0] = u0; v[3].uv[1] = v1;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
v[i].col[0] = cr; v[i].col[1] = cg; v[i].col[2] = cb; v[i].col[3] = ca;
|
||||
v[i].rect_min[0] = rmin_x; v[i].rect_min[1] = rmin_y;
|
||||
v[i].rect_max[0] = rmax_x; v[i].rect_max[1] = rmax_y;
|
||||
v[i].corner_radii[0] = cr_tl; v[i].corner_radii[1] = cr_tr;
|
||||
v[i].corner_radii[2] = cr_br; v[i].corner_radii[3] = cr_bl;
|
||||
v[i].border_thickness = border_thickness;
|
||||
v[i].softness = softness;
|
||||
v[i].mode = mode;
|
||||
}
|
||||
|
||||
U32 *idx = &batch->indices[batch->index_count];
|
||||
idx[0] = base; idx[1] = base + 1; idx[2] = base + 2;
|
||||
idx[3] = base; idx[4] = base + 2; idx[5] = base + 3;
|
||||
|
||||
batch->vertex_count += 4;
|
||||
batch->index_count += 6;
|
||||
}
|
||||
|
||||
static void emit_rect(DrawBatch *batch,
|
||||
float x0, float y0, float x1, float y1,
|
||||
float cr, float cg, float cb, float ca,
|
||||
float cr_tl, float cr_tr, float cr_br, float cr_bl,
|
||||
float border_thickness, float softness)
|
||||
{
|
||||
emit_quad(batch, x0, y0, x1, y1,
|
||||
0, 0, 0, 0,
|
||||
cr, cg, cb, ca,
|
||||
x0, y0, x1, y1,
|
||||
cr_tl, cr_tr, cr_br, cr_bl,
|
||||
border_thickness, softness, 0.0f);
|
||||
}
|
||||
|
||||
static void emit_rect_vgradient(DrawBatch *batch,
|
||||
float x0, float y0, float x1, float y1,
|
||||
float tr, float tg, float tb, float ta,
|
||||
float br, float bg, float bb_, float ba,
|
||||
float cr_tl, float cr_tr, float cr_br, float cr_bl,
|
||||
float softness)
|
||||
{
|
||||
if (batch->vertex_count + 4 > MAX_VERTICES || batch->index_count + 6 > MAX_INDICES)
|
||||
return;
|
||||
|
||||
U32 base = batch->vertex_count;
|
||||
UIVertex *v = &batch->vertices[base];
|
||||
|
||||
float pad = softness + 1.0f;
|
||||
float px0 = x0 - pad, py0 = y0 - pad, px1 = x1 + pad, py1 = y1 + pad;
|
||||
|
||||
v[0].pos[0] = px0; v[0].pos[1] = py0; v[0].uv[0] = 0; v[0].uv[1] = 0;
|
||||
v[1].pos[0] = px1; v[1].pos[1] = py0; v[1].uv[0] = 0; v[1].uv[1] = 0;
|
||||
v[2].pos[0] = px1; v[2].pos[1] = py1; v[2].uv[0] = 0; v[2].uv[1] = 0;
|
||||
v[3].pos[0] = px0; v[3].pos[1] = py1; v[3].uv[0] = 0; v[3].uv[1] = 0;
|
||||
|
||||
v[0].col[0] = tr; v[0].col[1] = tg; v[0].col[2] = tb; v[0].col[3] = ta;
|
||||
v[1].col[0] = tr; v[1].col[1] = tg; v[1].col[2] = tb; v[1].col[3] = ta;
|
||||
v[2].col[0] = br; v[2].col[1] = bg; v[2].col[2] = bb_; v[2].col[3] = ba;
|
||||
v[3].col[0] = br; v[3].col[1] = bg; v[3].col[2] = bb_; v[3].col[3] = ba;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
v[i].rect_min[0] = x0; v[i].rect_min[1] = y0;
|
||||
v[i].rect_max[0] = x1; v[i].rect_max[1] = y1;
|
||||
v[i].corner_radii[0] = cr_tl; v[i].corner_radii[1] = cr_tr;
|
||||
v[i].corner_radii[2] = cr_br; v[i].corner_radii[3] = cr_bl;
|
||||
v[i].border_thickness = 0;
|
||||
v[i].softness = softness;
|
||||
v[i].mode = 0;
|
||||
}
|
||||
|
||||
U32 *idx = &batch->indices[batch->index_count];
|
||||
idx[0] = base; idx[1] = base + 1; idx[2] = base + 2;
|
||||
idx[3] = base; idx[4] = base + 2; idx[5] = base + 3;
|
||||
|
||||
batch->vertex_count += 4;
|
||||
batch->index_count += 6;
|
||||
}
|
||||
|
||||
static void emit_text_glyphs(DrawBatch *batch, Renderer *r,
|
||||
Clay_BoundingBox bbox, Clay_Color color, const char *text, int32_t text_len,
|
||||
uint16_t font_size)
|
||||
{
|
||||
if (text_len == 0 || color.a < 0.1f) return;
|
||||
|
||||
float cr = color.r / 255.f;
|
||||
float cg = color.g / 255.f;
|
||||
float cb = color.b / 255.f;
|
||||
float ca = color.a / 255.f;
|
||||
|
||||
F32 scale = (F32)font_size / r->font_atlas_size;
|
||||
F32 text_h = r->font_line_height * scale;
|
||||
|
||||
F32 x = floorf(bbox.x + 0.5f);
|
||||
F32 y = floorf(bbox.y + (bbox.height - text_h) * 0.5f + 0.5f);
|
||||
|
||||
for (int32_t i = 0; i < text_len; i++) {
|
||||
char ch = text[i];
|
||||
if (ch < GLYPH_FIRST || ch > GLYPH_LAST) {
|
||||
if (ch == ' ') {
|
||||
int gi = ' ' - GLYPH_FIRST;
|
||||
if (gi >= 0 && gi < GLYPH_COUNT)
|
||||
x += r->glyphs[gi].x_advance * scale;
|
||||
continue;
|
||||
}
|
||||
ch = '?';
|
||||
}
|
||||
int gi = ch - GLYPH_FIRST;
|
||||
if (gi < 0 || gi >= GLYPH_COUNT) continue;
|
||||
|
||||
GlyphInfo *g = &r->glyphs[gi];
|
||||
F32 gw = g->w * scale;
|
||||
F32 gh = g->h * scale;
|
||||
|
||||
emit_quad(batch,
|
||||
x, y, x + gw, y + gh,
|
||||
g->u0, g->v0, g->u1, g->v1,
|
||||
cr, cg, cb, ca,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 1.0f);
|
||||
|
||||
x += g->x_advance * scale;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Public API
|
||||
|
||||
Renderer *renderer_create(RendererDesc *desc) {
|
||||
Renderer *r = new Renderer();
|
||||
memset(r, 0, sizeof(*r));
|
||||
|
||||
r->width = desc->width;
|
||||
r->height = desc->height;
|
||||
r->frame_count = desc->frame_count;
|
||||
if (r->frame_count > NUM_BACK_BUFFERS) r->frame_count = NUM_BACK_BUFFERS;
|
||||
|
||||
// Get the NSView and attach a CAMetalLayer
|
||||
NSView *view = (__bridge NSView *)desc->window_handle;
|
||||
[view setWantsLayer:YES];
|
||||
|
||||
r->device = MTLCreateSystemDefaultDevice();
|
||||
if (!r->device) { delete r; return nullptr; }
|
||||
|
||||
r->command_queue = [r->device newCommandQueue];
|
||||
|
||||
CAMetalLayer *layer = [CAMetalLayer layer];
|
||||
layer.device = r->device;
|
||||
layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
|
||||
layer.framebufferOnly = YES;
|
||||
|
||||
NSWindow *window = [view window];
|
||||
r->backing_scale = (F32)[window backingScaleFactor];
|
||||
layer.contentsScale = r->backing_scale;
|
||||
layer.drawableSize = CGSizeMake(r->width, r->height);
|
||||
|
||||
[view setLayer:layer];
|
||||
r->metal_layer = layer;
|
||||
|
||||
r->frame_semaphore = dispatch_semaphore_create(NUM_BACK_BUFFERS);
|
||||
|
||||
// Compile shaders
|
||||
NSError *error = nil;
|
||||
id<MTLLibrary> library = [r->device newLibraryWithSource:
|
||||
[NSString stringWithUTF8String:g_shader_msl] options:nil error:&error];
|
||||
if (!library) {
|
||||
NSLog(@"Metal shader compile error: %@", error);
|
||||
delete r;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
id<MTLFunction> vert_fn = [library newFunctionWithName:@"vertex_main"];
|
||||
id<MTLFunction> frag_fn = [library newFunctionWithName:@"fragment_main"];
|
||||
|
||||
// Vertex descriptor
|
||||
MTLVertexDescriptor *vtx_desc = [[MTLVertexDescriptor alloc] init];
|
||||
vtx_desc.attributes[0].format = MTLVertexFormatFloat2;
|
||||
vtx_desc.attributes[0].offset = offsetof(UIVertex, pos);
|
||||
vtx_desc.attributes[0].bufferIndex = 0;
|
||||
vtx_desc.attributes[1].format = MTLVertexFormatFloat2;
|
||||
vtx_desc.attributes[1].offset = offsetof(UIVertex, uv);
|
||||
vtx_desc.attributes[1].bufferIndex = 0;
|
||||
vtx_desc.attributes[2].format = MTLVertexFormatFloat4;
|
||||
vtx_desc.attributes[2].offset = offsetof(UIVertex, col);
|
||||
vtx_desc.attributes[2].bufferIndex = 0;
|
||||
vtx_desc.attributes[3].format = MTLVertexFormatFloat2;
|
||||
vtx_desc.attributes[3].offset = offsetof(UIVertex, rect_min);
|
||||
vtx_desc.attributes[3].bufferIndex = 0;
|
||||
vtx_desc.attributes[4].format = MTLVertexFormatFloat2;
|
||||
vtx_desc.attributes[4].offset = offsetof(UIVertex, rect_max);
|
||||
vtx_desc.attributes[4].bufferIndex = 0;
|
||||
vtx_desc.attributes[5].format = MTLVertexFormatFloat4;
|
||||
vtx_desc.attributes[5].offset = offsetof(UIVertex, corner_radii);
|
||||
vtx_desc.attributes[5].bufferIndex = 0;
|
||||
vtx_desc.attributes[6].format = MTLVertexFormatFloat;
|
||||
vtx_desc.attributes[6].offset = offsetof(UIVertex, border_thickness);
|
||||
vtx_desc.attributes[6].bufferIndex = 0;
|
||||
vtx_desc.attributes[7].format = MTLVertexFormatFloat;
|
||||
vtx_desc.attributes[7].offset = offsetof(UIVertex, softness);
|
||||
vtx_desc.attributes[7].bufferIndex = 0;
|
||||
vtx_desc.attributes[8].format = MTLVertexFormatFloat;
|
||||
vtx_desc.attributes[8].offset = offsetof(UIVertex, mode);
|
||||
vtx_desc.attributes[8].bufferIndex = 0;
|
||||
vtx_desc.layouts[0].stride = sizeof(UIVertex);
|
||||
vtx_desc.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
|
||||
|
||||
// Pipeline state
|
||||
MTLRenderPipelineDescriptor *pipe_desc = [[MTLRenderPipelineDescriptor alloc] init];
|
||||
pipe_desc.vertexFunction = vert_fn;
|
||||
pipe_desc.fragmentFunction = frag_fn;
|
||||
pipe_desc.vertexDescriptor = vtx_desc;
|
||||
pipe_desc.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
|
||||
pipe_desc.colorAttachments[0].blendingEnabled = YES;
|
||||
pipe_desc.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
|
||||
pipe_desc.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
|
||||
pipe_desc.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
|
||||
pipe_desc.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne;
|
||||
pipe_desc.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
|
||||
pipe_desc.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
|
||||
|
||||
r->pipeline_state = [r->device newRenderPipelineStateWithDescriptor:pipe_desc error:&error];
|
||||
if (!r->pipeline_state) {
|
||||
NSLog(@"Metal pipeline error: %@", error);
|
||||
delete r;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Create double-buffered vertex/index buffers
|
||||
for (int i = 0; i < NUM_BACK_BUFFERS; i++) {
|
||||
r->vertex_buffers[i] = [r->device newBufferWithLength:MAX_VERTICES * sizeof(UIVertex)
|
||||
options:MTLResourceStorageModeShared];
|
||||
r->index_buffers[i] = [r->device newBufferWithLength:MAX_INDICES * sizeof(U32)
|
||||
options:MTLResourceStorageModeShared];
|
||||
}
|
||||
|
||||
// Font atlas
|
||||
if (!create_font_atlas(r, 15.0f)) {
|
||||
delete r;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Init text measurement
|
||||
r->measure_font = nullptr;
|
||||
r->measure_font_size = 0;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
void renderer_destroy(Renderer *r) {
|
||||
if (!r) return;
|
||||
if (r->measure_font) CFRelease(r->measure_font);
|
||||
delete r;
|
||||
}
|
||||
|
||||
bool renderer_begin_frame(Renderer *r) {
|
||||
dispatch_semaphore_wait(r->frame_semaphore, DISPATCH_TIME_FOREVER);
|
||||
|
||||
id<CAMetalDrawable> drawable = [r->metal_layer nextDrawable];
|
||||
if (!drawable) {
|
||||
dispatch_semaphore_signal(r->frame_semaphore);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store drawable for end_frame (use a thread-local or just re-acquire)
|
||||
// We'll re-acquire in end_frame — nextDrawable is fast if we just called it
|
||||
dispatch_semaphore_signal(r->frame_semaphore);
|
||||
return true;
|
||||
}
|
||||
|
||||
void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
dispatch_semaphore_wait(r->frame_semaphore, DISPATCH_TIME_FOREVER);
|
||||
|
||||
@autoreleasepool {
|
||||
id<CAMetalDrawable> drawable = [r->metal_layer nextDrawable];
|
||||
if (!drawable) {
|
||||
dispatch_semaphore_signal(r->frame_semaphore);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t buf_idx = r->frame_index % NUM_BACK_BUFFERS;
|
||||
|
||||
MTLRenderPassDescriptor *pass = [MTLRenderPassDescriptor renderPassDescriptor];
|
||||
pass.colorAttachments[0].texture = drawable.texture;
|
||||
pass.colorAttachments[0].loadAction = MTLLoadActionClear;
|
||||
pass.colorAttachments[0].storeAction = MTLStoreActionStore;
|
||||
pass.colorAttachments[0].clearColor = MTLClearColorMake(0.12, 0.12, 0.13, 1.0);
|
||||
|
||||
id<MTLCommandBuffer> cmd_buf = [r->command_queue commandBuffer];
|
||||
id<MTLRenderCommandEncoder> encoder = [cmd_buf renderCommandEncoderWithDescriptor:pass];
|
||||
|
||||
[encoder setRenderPipelineState:r->pipeline_state];
|
||||
[encoder setFragmentTexture:r->font_texture atIndex:0];
|
||||
[encoder setFragmentSamplerState:r->font_sampler atIndex:0];
|
||||
|
||||
// Viewport
|
||||
MTLViewport viewport = {};
|
||||
viewport.width = (double)r->width;
|
||||
viewport.height = (double)r->height;
|
||||
viewport.zfar = 1.0;
|
||||
[encoder setViewport:viewport];
|
||||
|
||||
// Full scissor
|
||||
MTLScissorRect full_scissor = { 0, 0, (NSUInteger)r->width, (NSUInteger)r->height };
|
||||
[encoder setScissorRect:full_scissor];
|
||||
|
||||
// Constants
|
||||
float constants[2] = { (float)r->width, (float)r->height };
|
||||
[encoder setVertexBytes:constants length:sizeof(constants) atIndex:1];
|
||||
|
||||
// Process Clay render commands
|
||||
if (render_commands.length > 0) {
|
||||
DrawBatch batch = {};
|
||||
batch.vertices = (UIVertex *)[r->vertex_buffers[buf_idx] contents];
|
||||
batch.indices = (U32 *)[r->index_buffers[buf_idx] contents];
|
||||
batch.vertex_count = 0;
|
||||
batch.index_count = 0;
|
||||
|
||||
auto flush_batch = [&]() {
|
||||
if (batch.index_count == 0) return;
|
||||
|
||||
[encoder setVertexBuffer:r->vertex_buffers[buf_idx] offset:0 atIndex:0];
|
||||
[encoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle
|
||||
indexCount:batch.index_count
|
||||
indexType:MTLIndexTypeUInt32
|
||||
indexBuffer:r->index_buffers[buf_idx]
|
||||
indexBufferOffset:0];
|
||||
|
||||
batch.vertex_count = 0;
|
||||
batch.index_count = 0;
|
||||
};
|
||||
|
||||
for (int32_t i = 0; i < render_commands.length; i++) {
|
||||
Clay_RenderCommand *cmd = Clay_RenderCommandArray_Get(&render_commands, i);
|
||||
Clay_BoundingBox bb = cmd->boundingBox;
|
||||
|
||||
switch (cmd->commandType) {
|
||||
case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
|
||||
Clay_RectangleRenderData *rect = &cmd->renderData.rectangle;
|
||||
Clay_Color c = rect->backgroundColor;
|
||||
emit_rect(&batch,
|
||||
bb.x, bb.y, bb.x + bb.width, bb.y + bb.height,
|
||||
c.r / 255.f, c.g / 255.f, c.b / 255.f, c.a / 255.f,
|
||||
rect->cornerRadius.topLeft, rect->cornerRadius.topRight,
|
||||
rect->cornerRadius.bottomRight, rect->cornerRadius.bottomLeft,
|
||||
0, 1.0f);
|
||||
} break;
|
||||
|
||||
case CLAY_RENDER_COMMAND_TYPE_BORDER: {
|
||||
Clay_BorderRenderData *border = &cmd->renderData.border;
|
||||
Clay_Color c = border->color;
|
||||
float cr_norm = c.r / 255.f;
|
||||
float cg_norm = c.g / 255.f;
|
||||
float cb_norm = c.b / 255.f;
|
||||
float ca_norm = c.a / 255.f;
|
||||
|
||||
if (border->width.top > 0) {
|
||||
emit_rect(&batch, bb.x, bb.y, bb.x + bb.width, bb.y + border->width.top,
|
||||
cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f);
|
||||
}
|
||||
if (border->width.bottom > 0) {
|
||||
emit_rect(&batch, bb.x, bb.y + bb.height - border->width.bottom, bb.x + bb.width, bb.y + bb.height,
|
||||
cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f);
|
||||
}
|
||||
if (border->width.left > 0) {
|
||||
emit_rect(&batch, bb.x, bb.y, bb.x + border->width.left, bb.y + bb.height,
|
||||
cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f);
|
||||
}
|
||||
if (border->width.right > 0) {
|
||||
emit_rect(&batch, bb.x + bb.width - border->width.right, bb.y, bb.x + bb.width, bb.y + bb.height,
|
||||
cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f);
|
||||
}
|
||||
} break;
|
||||
|
||||
case CLAY_RENDER_COMMAND_TYPE_TEXT: {
|
||||
Clay_TextRenderData *text = &cmd->renderData.text;
|
||||
emit_text_glyphs(&batch, r, bb, text->textColor,
|
||||
text->stringContents.chars, text->stringContents.length,
|
||||
text->fontSize);
|
||||
} break;
|
||||
|
||||
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: {
|
||||
flush_batch();
|
||||
NSUInteger sx = (NSUInteger)Max(bb.x, 0.f);
|
||||
NSUInteger sy = (NSUInteger)Max(bb.y, 0.f);
|
||||
NSUInteger sw = (NSUInteger)Min(bb.width, (F32)r->width - (F32)sx);
|
||||
NSUInteger sh = (NSUInteger)Min(bb.height, (F32)r->height - (F32)sy);
|
||||
if (sw == 0) sw = 1;
|
||||
if (sh == 0) sh = 1;
|
||||
MTLScissorRect clip = { sx, sy, sw, sh };
|
||||
[encoder setScissorRect:clip];
|
||||
} break;
|
||||
|
||||
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: {
|
||||
flush_batch();
|
||||
[encoder setScissorRect:full_scissor];
|
||||
} break;
|
||||
|
||||
case CLAY_RENDER_COMMAND_TYPE_CUSTOM: {
|
||||
Clay_CustomRenderData *custom = &cmd->renderData.custom;
|
||||
if (custom->customData) {
|
||||
CustomRenderType type = *(CustomRenderType *)custom->customData;
|
||||
if (type == CUSTOM_RENDER_VGRADIENT) {
|
||||
CustomGradientData *grad = (CustomGradientData *)custom->customData;
|
||||
Clay_Color tc = grad->top_color;
|
||||
Clay_Color bc = grad->bottom_color;
|
||||
emit_rect_vgradient(&batch,
|
||||
bb.x, bb.y, bb.x + bb.width, bb.y + bb.height,
|
||||
tc.r / 255.f, tc.g / 255.f, tc.b / 255.f, tc.a / 255.f,
|
||||
bc.r / 255.f, bc.g / 255.f, bc.b / 255.f, bc.a / 255.f,
|
||||
custom->cornerRadius.topLeft, custom->cornerRadius.topRight,
|
||||
custom->cornerRadius.bottomRight, custom->cornerRadius.bottomLeft,
|
||||
1.0f);
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case CLAY_RENDER_COMMAND_TYPE_IMAGE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
flush_batch();
|
||||
}
|
||||
|
||||
[encoder endEncoding];
|
||||
[cmd_buf presentDrawable:drawable];
|
||||
|
||||
__block dispatch_semaphore_t sem = r->frame_semaphore;
|
||||
[cmd_buf addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull) {
|
||||
dispatch_semaphore_signal(sem);
|
||||
}];
|
||||
|
||||
[cmd_buf commit];
|
||||
}
|
||||
|
||||
r->frame_index++;
|
||||
}
|
||||
|
||||
void renderer_resize(Renderer *r, int32_t width, int32_t height) {
|
||||
if (width <= 0 || height <= 0) return;
|
||||
r->width = width;
|
||||
r->height = height;
|
||||
|
||||
NSWindow *window = [(__bridge NSView *)r->metal_layer.delegate window];
|
||||
if (window) {
|
||||
r->backing_scale = (F32)[window backingScaleFactor];
|
||||
r->metal_layer.contentsScale = r->backing_scale;
|
||||
}
|
||||
r->metal_layer.drawableSize = CGSizeMake(width, height);
|
||||
}
|
||||
Reference in New Issue
Block a user