begin macos port
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
# Build output
|
||||
build/
|
||||
build
|
||||
build_debug
|
||||
build_release
|
||||
|
||||
# debugger files
|
||||
*.raddbg
|
||||
@@ -7,6 +9,7 @@ build/
|
||||
|
||||
# bullshit files
|
||||
nul
|
||||
.DS_Store
|
||||
|
||||
# editor files
|
||||
*.sublime-workspace
|
||||
|
||||
13
.vscode/launch.json
vendored
13
.vscode/launch.json
vendored
@@ -2,10 +2,19 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug autosample",
|
||||
"name": "Debug autosample (macOS)",
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/build_debug/autosample.app/Contents/MacOS/autosample",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"preLaunchTask": "build-debug"
|
||||
},
|
||||
{
|
||||
"name": "Debug autosample (Windows)",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/build/autosample.exe",
|
||||
"program": "${workspaceFolder}/build_debug/autosample.exe",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
|
||||
70
.vscode/tasks.json
vendored
70
.vscode/tasks.json
vendored
@@ -2,48 +2,76 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "bootstrap nob",
|
||||
"label": "bootstrap",
|
||||
"type": "shell",
|
||||
"command": "cl",
|
||||
"args": ["/nologo", "nob.c"],
|
||||
"command": "cc",
|
||||
"args": ["build.c", "-o", "build"],
|
||||
"windows": {
|
||||
"command": "cl",
|
||||
"args": ["/nologo", "build.c"]
|
||||
},
|
||||
"group": "build",
|
||||
"problemMatcher": "$msCompile",
|
||||
"detail": "One-time bootstrap: compile nob.c with cl.exe"
|
||||
"problemMatcher": [],
|
||||
"detail": "One-time bootstrap: compile build.c"
|
||||
},
|
||||
{
|
||||
"label": "build",
|
||||
"type": "shell",
|
||||
"command": "${workspaceFolder}/nob.exe",
|
||||
"command": "${workspaceFolder}/build",
|
||||
"windows": {
|
||||
"command": "${workspaceFolder}/build.exe"
|
||||
},
|
||||
"group": "build",
|
||||
"problemMatcher": "$msCompile",
|
||||
"problemMatcher": {
|
||||
"owner": "cpp",
|
||||
"fileLocation": ["relative", "${workspaceFolder}"],
|
||||
"pattern": {
|
||||
"regexp": "^(.+?):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"message": 5
|
||||
}
|
||||
},
|
||||
"detail": "Build autosample (release)"
|
||||
},
|
||||
{
|
||||
"label": "build-debug",
|
||||
"type": "shell",
|
||||
"command": "${workspaceFolder}/nob.exe",
|
||||
"command": "${workspaceFolder}/build",
|
||||
"args": ["debug"],
|
||||
"windows": {
|
||||
"command": "${workspaceFolder}/build.exe",
|
||||
"args": ["debug"]
|
||||
},
|
||||
"group": "build",
|
||||
"problemMatcher": "$msCompile",
|
||||
"problemMatcher": {
|
||||
"owner": "cpp",
|
||||
"fileLocation": ["relative", "${workspaceFolder}"],
|
||||
"pattern": {
|
||||
"regexp": "^(.+?):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"message": 5
|
||||
}
|
||||
},
|
||||
"detail": "Build autosample (debug)"
|
||||
},
|
||||
{
|
||||
"label": "rebuild",
|
||||
"type": "shell",
|
||||
"command": "${workspaceFolder}/nob.exe",
|
||||
"group": "build",
|
||||
"problemMatcher": "$msCompile",
|
||||
"detail": "Clean and rebuild",
|
||||
"dependsOn": "clean"
|
||||
},
|
||||
{
|
||||
"label": "clean",
|
||||
"type": "shell",
|
||||
"command": "if (Test-Path build) { Remove-Item -Recurse -Force build }",
|
||||
"args": [],
|
||||
"command": "rm",
|
||||
"args": ["-rf", "build_debug", "build_release"],
|
||||
"windows": {
|
||||
"command": "PowerShell",
|
||||
"args": ["-Command", "Remove-Item -Recurse -Force -ErrorAction SilentlyContinue build_debug, build_release"]
|
||||
},
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"detail": "Remove build directory"
|
||||
"detail": "Remove build directories"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
74
README.md
74
README.md
@@ -4,21 +4,33 @@ Graphical interface for automatically recording samples from existing analog aud
|
||||
|
||||
## Build
|
||||
|
||||
### Windows
|
||||
|
||||
Requires MSVC (Visual Studio 2019 Build Tools or later) with the Windows SDK and C++20 support.
|
||||
|
||||
Open a Developer Command Prompt, then:
|
||||
|
||||
```
|
||||
cl /nologo nob.c
|
||||
nob.exe debug
|
||||
build\autosample.exe
|
||||
cl /nologo build.c
|
||||
build.exe debug
|
||||
build_debug\autosample.exe
|
||||
```
|
||||
|
||||
The first command is a one-time bootstrap. After that, `nob.exe` detects changes to `nob.c` and rebuilds itself automatically. Pass `debug` for a debug build (default is release). Pass `clean` to wipe the build directory.
|
||||
### macOS
|
||||
|
||||
Requires Xcode Command Line Tools (`xcode-select --install`).
|
||||
|
||||
```
|
||||
cc build.c -o build
|
||||
./build debug
|
||||
open build_debug/autosample.app
|
||||
```
|
||||
|
||||
The first command is a one-time bootstrap. After that, `./build` detects changes to `build.c` and rebuilds itself automatically. Pass `debug` for a debug build (output in `build_debug/`) or omit for release (`build_release/`). Pass `clean` to wipe the build directory.
|
||||
|
||||
## Architecture
|
||||
|
||||
The project uses a **unity build**: `src/main.cpp` includes all other `.cpp` files, producing a single translation unit compiled with one `$CC` invocation.
|
||||
The project uses a **unity build**: `src/main.cpp` includes all other `.cpp`/`.mm` files, producing a single translation unit compiled with one compiler invocation. Platform selection is handled with `#ifdef __APPLE__` guards.
|
||||
|
||||
### Base layer (`src/base/`)
|
||||
|
||||
@@ -26,7 +38,10 @@ Foundational types and utilities shared across the project. Provides sized integ
|
||||
|
||||
### Platform layer (`src/platform/`)
|
||||
|
||||
Abstracts window creation, event polling, and native handles behind a C-style API with opaque `PlatformWindow` handles. The Win32 backend (`platform_win32.cpp`) implements the interface. Other backends can be added without touching the rest of the code.
|
||||
Abstracts window creation, event polling, menus, clipboard, and native handles behind a C-style API with opaque `PlatformWindow` handles.
|
||||
|
||||
- **Windows**: `platform_win32.cpp` — Win32/HWND, `WM_*` message loop
|
||||
- **macOS**: `platform_macos.mm` — Cocoa/AppKit, NSWindow, NSTextInputClient
|
||||
|
||||
### UI layer (`src/ui/`)
|
||||
|
||||
@@ -39,26 +54,39 @@ The application layout is built in `main.cpp` using Clay macros directly. Panel
|
||||
|
||||
### Renderer (`src/renderer/`)
|
||||
|
||||
DirectX 12 renderer with a custom SDF-based pipeline for UI rendering. Processes Clay's `Clay_RenderCommandArray` output directly — no intermediate scene graph.
|
||||
Custom SDF-based pipeline for UI rendering. Processes Clay's `Clay_RenderCommandArray` output directly — no intermediate scene graph.
|
||||
|
||||
- **Font atlas**: Built at startup using GDI (`TextOutA` into a DIB section), converted to a single-channel R8 texture. Covers ASCII 32–126 (Segoe UI).
|
||||
- **Text measurement**: GDI-based (`GetTextExtentPoint32A`), exposed to Clay via the `Clay_SetMeasureTextFunction` callback.
|
||||
- **Render commands**: Handles `RECTANGLE` (SDF rounded rect with corner radius), `BORDER` (individual sides as thin rects), `TEXT` (glyph quads from the font atlas), and `SCISSOR_START`/`SCISSOR_END` (clip regions).
|
||||
- **Vertex format**: Position, UV, color, rect bounds, corner radius, border thickness, softness, and mode (0 = SDF rect, 1 = textured glyph). Alpha blending with premultiplied SDF anti-aliasing.
|
||||
- **Windows**: `renderer_dx12.cpp` — DirectX 12, HLSL shaders, GDI font atlas (Segoe UI)
|
||||
- **macOS**: `renderer_metal.mm` — Metal, MSL shaders, Core Text font atlas (SF Pro)
|
||||
|
||||
Both renderers share the same vertex format (18 floats), SDF rounded-rect shader, and Clay command processing logic. Text measurement uses GDI on Windows and Core Text on macOS.
|
||||
|
||||
### Audio (`src/audio/`)
|
||||
|
||||
Audio device enumeration and output with test tone generation.
|
||||
|
||||
- **Windows**: `audio_asio.cpp` — ASIO driver via COM, registry enumeration, multi-format sample writing
|
||||
- **macOS**: `audio_coreaudio.cpp` — CoreAudio with AUGraph/HAL Output, Float32 non-interleaved
|
||||
|
||||
### MIDI (`src/midi/`)
|
||||
|
||||
Enumerates MIDI input and output devices via the Win32 multimedia API (`midiInGetDevCapsA` / `midiOutGetDevCapsA`). Provides a simple `MidiEngine` with device listing and refresh.
|
||||
MIDI device enumeration with real-time input monitoring.
|
||||
|
||||
- **Windows**: `midi_win32.cpp` — Win32 multimedia API (`midiIn*` / `midiOut*`)
|
||||
- **macOS**: `midi_coremidi.cpp` — CoreMIDI client with single input port, source connection
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
nob.c Build script (compiled to nob.exe)
|
||||
nob.h nob build system (vendored, single-header)
|
||||
build.c Build script (cc build.c -o nob)
|
||||
vendor/
|
||||
nob/
|
||||
nob.h nob build system (vendored, single-header)
|
||||
clay/
|
||||
clay.h Clay v0.14 (modified for MSVC C++ compatibility)
|
||||
src/
|
||||
main.cpp Entry point, unity build includes, Clay layout
|
||||
menus.cpp Menu bar setup
|
||||
theme.cpp Theme stub
|
||||
base/
|
||||
base_core.h Sized types, macros
|
||||
base_arena.h / .cpp Arena allocator
|
||||
@@ -68,19 +96,23 @@ src/
|
||||
platform/
|
||||
platform.h Window management API (platform-agnostic)
|
||||
platform_win32.cpp Win32 implementation
|
||||
platform_macos.mm macOS Cocoa implementation
|
||||
renderer/
|
||||
renderer.h Renderer API (graphics-agnostic)
|
||||
renderer_dx12.cpp DirectX 12 implementation, SDF pipeline, font atlas
|
||||
renderer_dx12.cpp DirectX 12 implementation
|
||||
renderer_metal.mm Metal implementation
|
||||
ui/
|
||||
ui_core.h Clay wrapper types and lifecycle API
|
||||
ui_core.cpp Clay init, text measurement bridge, theme
|
||||
ui_widgets.h / .cpp Widget stubs (reserved)
|
||||
audio/
|
||||
audio.h Audio device and playback API
|
||||
audio_asio.cpp Windows ASIO implementation
|
||||
audio_coreaudio.cpp macOS CoreAudio implementation
|
||||
midi/
|
||||
midi.h MIDI device enumeration API
|
||||
midi_win32.cpp Win32 multimedia implementation
|
||||
vendor/
|
||||
clay/
|
||||
clay.h Clay v0.14 (modified for MSVC C++ compatibility)
|
||||
midi_coremidi.cpp macOS CoreMIDI implementation
|
||||
```
|
||||
|
||||
### Modifications to `vendor/clay/clay.h`
|
||||
@@ -95,7 +127,7 @@ These patches allow the entire project (including Clay) to compile as a single C
|
||||
|
||||
## Code style
|
||||
|
||||
This project is written in C-style C++. We use `.cpp` files and a small subset of C++ features (default struct values, function overloading, namespaces where useful) but avoid the rest. No classes, no inheritance, limit templates, no exceptions, no RAII, avoid STL containers or algorithms. Data is plain structs. Functions operate on those structs, or pointers to them.
|
||||
This project is written in C-style C++. We use `.cpp` files and a small subset of C++ features (default struct values, function overloading, namespaces where useful) but avoid the rest. No classes, no inheritance, limit templates, no exceptions, no RTTI, avoid STL containers or algorithms. Data is plain structs. Functions operate on those structs, or pointers to them. macOS files use `.mm` extension for Objective-C++ interop.
|
||||
|
||||
Memory should be managed with arena allocators where possible rather than individual `malloc`/`free` or `new`/`delete` calls. Arenas make allocation fast, avoid fragmentation, and simplify cleanup.
|
||||
|
||||
@@ -106,7 +138,7 @@ Memory should be managed with arena allocators where possible rather than indivi
|
||||
|
||||
## Dependencies
|
||||
|
||||
All dependencies are vendored as source. Nothing to download or install beyond the Windows SDK.
|
||||
All dependencies are vendored as source. On Windows, nothing to install beyond the Windows SDK. On macOS, only Xcode Command Line Tools are needed — all frameworks (Metal, Cocoa, CoreAudio, CoreMIDI, CoreText, etc.) ship with the OS.
|
||||
|
||||
- [nob.h](https://github.com/tsoding/nob.h) — build system
|
||||
- [Clay](https://github.com/nicbarker/clay) — single-header C layout library (v0.14, with MSVC C++ patches)
|
||||
|
||||
222
build.c
Normal file
222
build.c
Normal file
@@ -0,0 +1,222 @@
|
||||
// Bootstrap:
|
||||
// Windows: cl /nologo build.c
|
||||
// macOS: cc build.c -o build
|
||||
// After that, just run: ./build (or build.exe on Windows)
|
||||
|
||||
#define NOB_IMPLEMENTATION
|
||||
#include "vendor/nob/nob.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
////////////////////////////////
|
||||
// macOS build (clang++ with Objective-C++)
|
||||
|
||||
static const char *frameworks[] = {
|
||||
"-framework", "Metal",
|
||||
"-framework", "Cocoa",
|
||||
"-framework", "CoreAudio",
|
||||
"-framework", "AudioToolbox",
|
||||
"-framework", "CoreMIDI",
|
||||
"-framework", "QuartzCore",
|
||||
"-framework", "CoreText",
|
||||
"-framework", "CoreFoundation",
|
||||
"-framework", "CoreGraphics",
|
||||
};
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
NOB_GO_REBUILD_URSELF(argc, argv);
|
||||
|
||||
bool debug = false;
|
||||
bool clean = false;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "debug") == 0) debug = true;
|
||||
else if (strcmp(argv[i], "clean") == 0) clean = true;
|
||||
}
|
||||
|
||||
const char *build_dir = debug ? "build_debug" : "build_release";
|
||||
|
||||
if (clean) {
|
||||
nob_log(NOB_INFO, "Cleaning %s/", build_dir);
|
||||
Nob_Cmd cmd = {0};
|
||||
nob_cmd_append(&cmd, "rm", "-rf", build_dir);
|
||||
{ Nob_Cmd_Opt opt = {0}; if (!nob_cmd_run_opt(&cmd, opt)) return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
// App bundle paths
|
||||
const char *app_dir = nob_temp_sprintf("%s/autosample.app", build_dir);
|
||||
const char *contents = nob_temp_sprintf("%s/Contents", app_dir);
|
||||
const char *macos_dir = nob_temp_sprintf("%s/Contents/MacOS", app_dir);
|
||||
const char *res_dir = nob_temp_sprintf("%s/Contents/Resources", app_dir);
|
||||
const char *binary_path = nob_temp_sprintf("%s/Contents/MacOS/autosample", app_dir);
|
||||
|
||||
if (!nob_mkdir_if_not_exists(build_dir)) return 1;
|
||||
if (!nob_mkdir_if_not_exists(app_dir)) return 1;
|
||||
if (!nob_mkdir_if_not_exists(contents)) return 1;
|
||||
if (!nob_mkdir_if_not_exists(macos_dir)) return 1;
|
||||
if (!nob_mkdir_if_not_exists(res_dir)) return 1;
|
||||
|
||||
// Unity build: single clang++ invocation compiles main.cpp (which #includes everything)
|
||||
{
|
||||
Nob_Cmd cmd = {0};
|
||||
nob_cmd_append(&cmd, "clang++");
|
||||
nob_cmd_append(&cmd, "-std=c++20", "-x", "objective-c++");
|
||||
nob_cmd_append(&cmd, "-fno-exceptions", "-fno-rtti");
|
||||
nob_cmd_append(&cmd, "-Wall", "-Wextra", "-Wno-missing-field-initializers");
|
||||
nob_cmd_append(&cmd, "-Wno-deprecated-declarations");
|
||||
nob_cmd_append(&cmd, "-Isrc", "-Ivendor/clay");
|
||||
|
||||
if (debug) {
|
||||
nob_cmd_append(&cmd, "-g", "-O0", "-D_DEBUG");
|
||||
} else {
|
||||
nob_cmd_append(&cmd, "-O2", "-DNDEBUG");
|
||||
}
|
||||
|
||||
nob_cmd_append(&cmd, "-o", binary_path);
|
||||
nob_cmd_append(&cmd, "src/main.cpp");
|
||||
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < NOB_ARRAY_LEN(frameworks); i++)
|
||||
nob_cmd_append(&cmd, frameworks[i]);
|
||||
}
|
||||
{ Nob_Cmd_Opt opt = {0}; if (!nob_cmd_run_opt(&cmd, opt)) return 1; }
|
||||
}
|
||||
|
||||
// Write Info.plist
|
||||
{
|
||||
const char *plist_path = nob_temp_sprintf("%s/Contents/Info.plist", app_dir);
|
||||
nob_write_entire_file(plist_path,
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\"\n"
|
||||
" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
|
||||
"<plist version=\"1.0\">\n"
|
||||
"<dict>\n"
|
||||
" <key>CFBundleExecutable</key>\n"
|
||||
" <string>autosample</string>\n"
|
||||
" <key>CFBundleIdentifier</key>\n"
|
||||
" <string>com.autosample.app</string>\n"
|
||||
" <key>CFBundleName</key>\n"
|
||||
" <string>autosample</string>\n"
|
||||
" <key>CFBundleVersion</key>\n"
|
||||
" <string>0.1</string>\n"
|
||||
" <key>CFBundleShortVersionString</key>\n"
|
||||
" <string>0.1</string>\n"
|
||||
" <key>CFBundlePackageType</key>\n"
|
||||
" <string>APPL</string>\n"
|
||||
" <key>NSHighResolutionCapable</key>\n"
|
||||
" <true/>\n"
|
||||
" <key>NSSupportsAutomaticGraphicsSwitching</key>\n"
|
||||
" <true/>\n"
|
||||
"</dict>\n"
|
||||
"</plist>\n",
|
||||
strlen(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\"\n"
|
||||
" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
|
||||
"<plist version=\"1.0\">\n"
|
||||
"<dict>\n"
|
||||
" <key>CFBundleExecutable</key>\n"
|
||||
" <string>autosample</string>\n"
|
||||
" <key>CFBundleIdentifier</key>\n"
|
||||
" <string>com.autosample.app</string>\n"
|
||||
" <key>CFBundleName</key>\n"
|
||||
" <string>autosample</string>\n"
|
||||
" <key>CFBundleVersion</key>\n"
|
||||
" <string>0.1</string>\n"
|
||||
" <key>CFBundleShortVersionString</key>\n"
|
||||
" <string>0.1</string>\n"
|
||||
" <key>CFBundlePackageType</key>\n"
|
||||
" <string>APPL</string>\n"
|
||||
" <key>NSHighResolutionCapable</key>\n"
|
||||
" <true/>\n"
|
||||
" <key>NSSupportsAutomaticGraphicsSwitching</key>\n"
|
||||
" <true/>\n"
|
||||
"</dict>\n"
|
||||
"</plist>\n"
|
||||
));
|
||||
}
|
||||
|
||||
nob_log(NOB_INFO, "Build complete: %s", app_dir);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#else
|
||||
////////////////////////////////
|
||||
// Windows build (MSVC cl.exe)
|
||||
|
||||
static const char *link_libs[] = {
|
||||
"d3d12.lib",
|
||||
"dxgi.lib",
|
||||
"d3dcompiler.lib",
|
||||
"user32.lib",
|
||||
"gdi32.lib",
|
||||
"shell32.lib",
|
||||
"ole32.lib",
|
||||
"advapi32.lib",
|
||||
"dwmapi.lib",
|
||||
"winmm.lib",
|
||||
};
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
NOB_GO_REBUILD_URSELF(argc, argv);
|
||||
|
||||
bool debug = false;
|
||||
bool clean = false;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "debug") == 0) debug = true;
|
||||
else if (strcmp(argv[i], "clean") == 0) clean = true;
|
||||
}
|
||||
|
||||
const char *build_dir = debug ? "build_debug" : "build_release";
|
||||
|
||||
if (clean) {
|
||||
nob_log(NOB_INFO, "Cleaning %s/", build_dir);
|
||||
Nob_Cmd cmd = {0};
|
||||
nob_cmd_append(&cmd, "cmd.exe", "/c",
|
||||
nob_temp_sprintf("if exist %s rmdir /s /q %s", build_dir, build_dir));
|
||||
{ Nob_Cmd_Opt opt = {0}; if (!nob_cmd_run_opt(&cmd, opt)) return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!nob_mkdir_if_not_exists(build_dir)) return 1;
|
||||
|
||||
// Unity build: single cl.exe invocation compiles main.cpp (which #includes everything)
|
||||
{
|
||||
Nob_Cmd cmd = {0};
|
||||
nob_cmd_append(&cmd, "cl.exe");
|
||||
nob_cmd_append(&cmd, "/nologo", "/std:c++20", "/EHsc", "/W3");
|
||||
nob_cmd_append(&cmd, "/Isrc", "/Ivendor/clay");
|
||||
|
||||
if (debug) {
|
||||
nob_cmd_append(&cmd, "/MTd", "/Zi", "/Od", "/D_DEBUG");
|
||||
} else {
|
||||
nob_cmd_append(&cmd, "/MT", "/Zi", "/O2", "/DNDEBUG");
|
||||
}
|
||||
|
||||
nob_cmd_append(&cmd, nob_temp_sprintf("/Fe:%s/autosample.exe", build_dir));
|
||||
nob_cmd_append(&cmd, nob_temp_sprintf("/Fo:%s/", build_dir));
|
||||
nob_cmd_append(&cmd, nob_temp_sprintf("/Fd:%s/autosample.pdb", build_dir));
|
||||
|
||||
nob_cmd_append(&cmd, "src/main.cpp");
|
||||
|
||||
nob_cmd_append(&cmd, "/link");
|
||||
nob_cmd_append(&cmd, "/MACHINE:X64");
|
||||
nob_cmd_append(&cmd, "/SUBSYSTEM:CONSOLE");
|
||||
nob_cmd_append(&cmd, nob_temp_sprintf("/PDB:%s/autosample.pdb", build_dir));
|
||||
nob_cmd_append(&cmd, "/DEBUG");
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < NOB_ARRAY_LEN(link_libs); i++)
|
||||
nob_cmd_append(&cmd, link_libs[i]);
|
||||
}
|
||||
{ Nob_Cmd_Opt opt = {0}; if (!nob_cmd_run_opt(&cmd, opt)) return 1; }
|
||||
}
|
||||
|
||||
// Clean up obj files
|
||||
nob_delete_file(nob_temp_sprintf("%s/main.obj", build_dir));
|
||||
|
||||
nob_log(NOB_INFO, "Build complete: %s/autosample.exe", build_dir);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
79
nob.c
79
nob.c
@@ -1,79 +0,0 @@
|
||||
// Bootstrap: cl /nologo nob.c
|
||||
// After that, just run: nob.exe
|
||||
|
||||
#define NOB_IMPLEMENTATION
|
||||
#include "nob.h"
|
||||
|
||||
#define BUILD_DIR "build"
|
||||
|
||||
static const char *link_libs[] = {
|
||||
"d3d12.lib",
|
||||
"dxgi.lib",
|
||||
"d3dcompiler.lib",
|
||||
"user32.lib",
|
||||
"gdi32.lib",
|
||||
"shell32.lib",
|
||||
"ole32.lib",
|
||||
"advapi32.lib",
|
||||
"dwmapi.lib",
|
||||
"winmm.lib",
|
||||
};
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
NOB_GO_REBUILD_URSELF(argc, argv);
|
||||
|
||||
bool debug = false;
|
||||
bool clean = false;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "debug") == 0) debug = true;
|
||||
else if (strcmp(argv[i], "clean") == 0) clean = true;
|
||||
}
|
||||
|
||||
if (clean) {
|
||||
nob_log(NOB_INFO, "Cleaning %s/", BUILD_DIR);
|
||||
Nob_Cmd cmd = {0};
|
||||
nob_cmd_append(&cmd, "cmd.exe", "/c", "if exist " BUILD_DIR " rmdir /s /q " BUILD_DIR);
|
||||
{ Nob_Cmd_Opt opt = {0}; if (!nob_cmd_run_opt(&cmd, opt)) return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!nob_mkdir_if_not_exists(BUILD_DIR)) return 1;
|
||||
|
||||
// Unity build: single cl.exe invocation compiles main.cpp (which #includes everything)
|
||||
{
|
||||
Nob_Cmd cmd = {0};
|
||||
nob_cmd_append(&cmd, "cl.exe");
|
||||
nob_cmd_append(&cmd, "/nologo", "/std:c++20", "/EHsc", "/W3");
|
||||
nob_cmd_append(&cmd, "/Isrc", "/Ivendor/clay");
|
||||
|
||||
if (debug) {
|
||||
nob_cmd_append(&cmd, "/MTd", "/Zi", "/Od", "/D_DEBUG");
|
||||
} else {
|
||||
nob_cmd_append(&cmd, "/MT", "/Zi", "/O2", "/DNDEBUG");
|
||||
}
|
||||
|
||||
nob_cmd_append(&cmd, nob_temp_sprintf("/Fe:%s/autosample.exe", BUILD_DIR));
|
||||
nob_cmd_append(&cmd, nob_temp_sprintf("/Fo:%s/", BUILD_DIR));
|
||||
nob_cmd_append(&cmd, nob_temp_sprintf("/Fd:%s/autosample.pdb", BUILD_DIR));
|
||||
|
||||
nob_cmd_append(&cmd, "src/main.cpp");
|
||||
|
||||
nob_cmd_append(&cmd, "/link");
|
||||
nob_cmd_append(&cmd, "/MACHINE:X64");
|
||||
nob_cmd_append(&cmd, "/SUBSYSTEM:CONSOLE");
|
||||
nob_cmd_append(&cmd, nob_temp_sprintf("/PDB:%s/autosample.pdb", BUILD_DIR));
|
||||
nob_cmd_append(&cmd, "/DEBUG");
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < NOB_ARRAY_LEN(link_libs); i++)
|
||||
nob_cmd_append(&cmd, link_libs[i]);
|
||||
}
|
||||
{ Nob_Cmd_Opt opt = {0}; if (!nob_cmd_run_opt(&cmd, opt)) return 1; }
|
||||
}
|
||||
|
||||
// Clean up obj files
|
||||
nob_delete_file(nob_temp_sprintf("%s/main.obj", BUILD_DIR));
|
||||
|
||||
nob_log(NOB_INFO, "Build complete: %s/autosample.exe", BUILD_DIR);
|
||||
return 0;
|
||||
}
|
||||
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);
|
||||
}
|
||||
0
nob.h → vendor/nob/nob.h
vendored
0
nob.h → vendor/nob/nob.h
vendored
Reference in New Issue
Block a user