[0.1.25] macOS phase 1: builds and runs on a Mac, the library folder as a setting
This commit is contained in:
parent
8c8b2c6e38
commit
12d0f30fc4
13
README.md
13
README.md
@ -31,8 +31,10 @@ see [Screen capture](#screen-capture).
|
|||||||
3. **Build and run:** `jai first.jai - release`, then `bin\mcap.exe`. The build copies onnxruntime
|
3. **Build and run:** `jai first.jai - release`, then `bin\mcap.exe`. The build copies onnxruntime
|
||||||
and the models beside the program.
|
and the models beside the program.
|
||||||
|
|
||||||
4. **The library** is `D:\McapIndex\mcap`, a folder for each place imported from
|
4. **The library** is `D:\McapIndex\mcap` on Windows and `~/Pictures/mcap` on macOS and Linux, a
|
||||||
(`SnagitLibraryPersonal`, `SnagitLibraryWork`, ...); `-library FOLDER` shows another folder.
|
folder for each place imported from (`SnagitLibraryPersonal`, `SnagitLibraryWork`, ...).
|
||||||
|
`folder:` under `[library]` in `settings.config` names another, read when mcap starts, and
|
||||||
|
`-library FOLDER` overrides both.
|
||||||
**Import** (button above the grid) brings in more; see [Importing](#importing).
|
**Import** (button above the grid) brings in more; see [Importing](#importing).
|
||||||
|
|
||||||
5. **Read the library:** open the **OCR** window (button above the grid), check **Labels too**,
|
5. **Read the library:** open the **OCR** window (button above the grid), check **Labels too**,
|
||||||
@ -394,7 +396,7 @@ then in that order instead of by date.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `FILE` (first) | Opens FILE: an `.mcap`, image or video; in view mode when mcap is already running. See [Opening a file](#opening-a-file). |
|
| `FILE` (first) | Opens FILE: an `.mcap`, image or video; in view mode when mcap is already running. See [Opening a file](#opening-a-file). |
|
||||||
| `-open-import DEST` | With FILE: imports it into the library folder DEST as its Import button does, then exits: 0 once the library has it (imported, or there already), else 1. |
|
| `-open-import DEST` | With FILE: imports it into the library folder DEST as its Import button does, then exits: 0 once the library has it (imported, or there already), else 1. |
|
||||||
| `-library FOLDER` | Shows the `.mcap` files in FOLDER (and its folders) instead of `D:\McapIndex\mcap`. |
|
| `-library FOLDER` | Shows the `.mcap` files in FOLDER (and its folders) instead of the library folder in `settings.config` (`[library] folder`). |
|
||||||
| `-import SOURCE DEST` | Imports SOURCE, a folder or a single file, into the library folder DEST, as the Import window does, then exits: 0, or 1 if any file failed. |
|
| `-import SOURCE DEST` | Imports SOURCE, a folder or a single file, into the library folder DEST, as the Import window does, then exits: 0, or 1 if any file failed. |
|
||||||
| `-search QUERY` | Starts with QUERY in the search box. |
|
| `-search QUERY` | Starts with QUERY in the search box. |
|
||||||
| `-cold` | Loads the library as on a cold start: its files and the catalog cache are dropped from the system's file cache first (Windows and Linux; not macOS, which allows it only with `sudo purge`). The `[startup]` lines in the log time each step. With `-stats`, a startup benchmark: `bin\mcap.exe -cold -stats`. |
|
| `-cold` | Loads the library as on a cold start: its files and the catalog cache are dropped from the system's file cache first (Windows and Linux; not macOS, which allows it only with `sudo purge`). The `[startup]` lines in the log time each step. With `-stats`, a startup benchmark: `bin\mcap.exe -cold -stats`. |
|
||||||
@ -517,10 +519,11 @@ Binaries are written to `bin/`, with `assets/` copied beside them.
|
|||||||
| Platform | Output | Notes |
|
| Platform | Output | Notes |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Windows | `bin/mcap-debug.exe`, `bin/mcap.exe` | The release build gets the icon and a manifest, and no console. |
|
| Windows | `bin/mcap-debug.exe`, `bin/mcap.exe` | The release build gets the icon and a manifest, and no console. |
|
||||||
| macOS | `bin/mcap-macos-debug`, `bin/mcap.app` | Apple silicon (arm64) only, with a native `jai`. The release build is an ad hoc signed `.app`. |
|
| macOS | `bin/mcap-debug.app`, `bin/mcap.app` (and the bare `bin/mcap-macos-debug`, `bin/mcap-macos`) | Apple silicon (arm64) only, with a native `jai`; macOS 14 or later. Each `.app` holds onnxruntime and the models, and is signed with the self-signed "Ex4 Dev" identity when the keychain has it (made by Ex4's `scripts/macos_dev_identity.sh`), so macOS keeps its permissions across builds; otherwise ad hoc. |
|
||||||
| Linux | `bin/mcap-linux-debug`, `bin/mcap-linux` | Needs SDL3 installed (`libSDL3.so`). |
|
| Linux | `bin/mcap-linux-debug`, `bin/mcap-linux` | Needs SDL3 installed (`libSDL3.so`). |
|
||||||
|
|
||||||
`jai check_targets.jai - linux` type-checks the Linux build from any OS without linking it.
|
`jai check_targets.jai - linux` type-checks the Linux build from any OS without linking it (on the
|
||||||
|
host's CPU: from a Mac, Linux on arm64).
|
||||||
The macOS check only works on a Mac, since the Objective-C module runs code at compile time.
|
The macOS check only works on a Mac, since the Objective-C module runs code at compile time.
|
||||||
|
|
||||||
Run any build with `run_tests` to run the test suites (debug builds include them),
|
Run any build with `run_tests` to run the test suites (debug builds include them),
|
||||||
|
|||||||
@ -8,8 +8,10 @@
|
|||||||
#run,stallable {
|
#run,stallable {
|
||||||
set_build_options_dc(.{do_output = false});
|
set_build_options_dc(.{do_output = false});
|
||||||
args := get_build_options().compile_time_command_line;
|
args := get_build_options().compile_time_command_line;
|
||||||
|
// Linux on the host's CPU: the ARM compiler cannot run x64 code at compile time (String's #asm
|
||||||
|
// in the hotkey checks' #run), so from a Mac it checks Linux on arm64.
|
||||||
target := Operating_System_Tag.LINUX;
|
target := Operating_System_Tag.LINUX;
|
||||||
cpu := CPU_Tag.X64;
|
cpu := CPU;
|
||||||
if array_find(args, "macos") { target = .MACOS; cpu = .ARM64; }
|
if array_find(args, "macos") { target = .MACOS; cpu = .ARM64; }
|
||||||
|
|
||||||
options := get_build_options();
|
options := get_build_options();
|
||||||
@ -47,7 +49,7 @@ APP_NAME :: "mcap";
|
|||||||
APP_FOLDER_NAME :: "mcap";
|
APP_FOLDER_NAME :: "mcap";
|
||||||
GIT_BRANCH :: "check";
|
GIT_BRANCH :: "check";
|
||||||
GIT_HASH :: "-";
|
GIT_HASH :: "-";
|
||||||
VERSION :: "0.1.24";
|
VERSION :: "0.1.25";
|
||||||
BUILD_DATE :: "-";
|
BUILD_DATE :: "-";
|
||||||
DEBUG :: true;
|
DEBUG :: true;
|
||||||
COMPILE_TESTS :: true;
|
COMPILE_TESTS :: true;
|
||||||
|
|||||||
118
first.jai
118
first.jai
@ -8,7 +8,7 @@ APP_NAME :: "mcap";
|
|||||||
APP_FOLDER_NAME :: "mcap";
|
APP_FOLDER_NAME :: "mcap";
|
||||||
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
|
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
|
||||||
|
|
||||||
VERSION :: "0.1.24";
|
VERSION :: "0.1.25";
|
||||||
// Where everything goes: bin/, or bin-headless/ for builds made with no_crash_dialog, so automated
|
// Where everything goes: bin/, or bin-headless/ for builds made with no_crash_dialog, so automated
|
||||||
// builds never write over the ones you run by hand.
|
// builds never write over the ones you run by hand.
|
||||||
build_directory := "bin";
|
build_directory := "bin";
|
||||||
@ -125,6 +125,7 @@ start_main_build :: (optimized: bool) -> Pending_Build {
|
|||||||
#if CPU == .X64 options.backend = .X64; // the fastest to compile; LLVM elsewhere
|
#if CPU == .X64 options.backend = .X64; // the fastest to compile; LLVM elsewhere
|
||||||
options.output_executable_name = tprint("%-debug", output_executable_name_temp);
|
options.output_executable_name = tprint("%-debug", output_executable_name_temp);
|
||||||
}
|
}
|
||||||
|
#if OS == .MACOS options.minimum_os_version = MACOS_MINIMUM_VERSION;
|
||||||
|
|
||||||
#if OS == .WINDOWS {
|
#if OS == .WINDOWS {
|
||||||
file_delete(tprint("%/%.pdb", build_directory, options.output_executable_name));
|
file_delete(tprint("%/%.pdb", build_directory, options.output_executable_name));
|
||||||
@ -166,6 +167,7 @@ start_tool_build :: (name: string, file: string, optimized: bool) -> Pending_Bui
|
|||||||
options.output_executable_name = tprint("%-debug", name);
|
options.output_executable_name = tprint("%-debug", name);
|
||||||
}
|
}
|
||||||
options.output_path = build_directory;
|
options.output_path = build_directory;
|
||||||
|
#if OS == .MACOS options.minimum_os_version = MACOS_MINIMUM_VERSION;
|
||||||
|
|
||||||
w := compiler_create_workspace(options.output_executable_name);
|
w := compiler_create_workspace(options.output_executable_name);
|
||||||
set_build_options(options, w);
|
set_build_options(options, w);
|
||||||
@ -194,25 +196,47 @@ finish_main_build :: (build: Pending_Build) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else #if OS == .MACOS {
|
} else #if OS == .MACOS {
|
||||||
if build.optimized create_macos_app_bundle(build.executable_name);
|
create_macos_app_bundle(build.executable_name, build.optimized);
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Built %/%", build_directory, build.executable_name);
|
log("Built %/%", build_directory, build.executable_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if OS == .MACOS {
|
#if OS == .MACOS {
|
||||||
MACOS_APP_BUNDLE :: #run tprint("%.app", APP_NAME);
|
// The oldest macOS it runs on: ScreenCaptureKit's screenshots need 14, and so does onnxruntime 1.30.
|
||||||
|
MACOS_MINIMUM_VERSION :: Build_Options.Os_Version.{ major = 14, minor = 0 };
|
||||||
|
|
||||||
|
// A self-signed code-signing identity, made once per Mac by Ex4's scripts/macos_dev_identity.sh
|
||||||
|
// and shared with it. macOS ties Screen Recording and the other privacy permissions to an app's
|
||||||
|
// signature, and an ad hoc one changes with every build, so they would be asked for again each
|
||||||
|
// time. Without it in the keychain, the bundle is signed ad hoc.
|
||||||
|
MACOS_SIGNING_IDENTITY :: "Ex4 Dev";
|
||||||
|
|
||||||
MACOS_BUNDLE_DYLIBS :: string.["libSDL3.0.dylib"];
|
MACOS_BUNDLE_DYLIBS :: string.["libSDL3.0.dylib"];
|
||||||
|
|
||||||
// The .app holds only the executable, the dylibs it loads from beside itself, the icon and
|
// mcap.app for the release build, mcap-debug.app for the debug one, with an identifier of its own
|
||||||
// Info.plist. The app finds data/, logs/ and images/ in the folder holding the bundle; see
|
// so that its permissions are granted apart. Each holds the executable, SDL3 beside it,
|
||||||
// app_install_directory.
|
// onnxruntime in Contents/Frameworks, and the icon, Info.plist and models in Contents/Resources.
|
||||||
create_macos_app_bundle :: (executable_name: string) {
|
// The app finds data/, logs/ and images/ in the folder holding the bundle (see
|
||||||
app := tprint("%/%", build_directory, MACOS_APP_BUNDLE);
|
// app_install_directory), and the models in Contents/Resources (app_resource_directory).
|
||||||
run_command("rm", "-rf", app);
|
//
|
||||||
|
// The bundle is updated in place, not made afresh: the models are some 900 MB, so they are copied
|
||||||
|
// only when changed, as APFS clones, which take no space of their own.
|
||||||
|
//
|
||||||
|
// Started with `open`, the bundle asks for permissions as itself; the bare executable started
|
||||||
|
// from a terminal would have them asked for, and granted, to the terminal.
|
||||||
|
create_macos_app_bundle :: (executable_name: string, optimized: bool) {
|
||||||
|
bundle_name := ifx optimized then tprint("%.app", APP_NAME) else tprint("%-debug.app", APP_NAME);
|
||||||
|
identifier := ifx optimized then APP_BUNDLE_IDENTIFIER else tprint("%.debug", APP_BUNDLE_IDENTIFIER);
|
||||||
|
display := ifx optimized then APP_NAME else tprint("% Debug", APP_NAME);
|
||||||
|
app := tprint("%/%", build_directory, bundle_name);
|
||||||
executable_folder := tprint("%/Contents/MacOS", app);
|
executable_folder := tprint("%/Contents/MacOS", app);
|
||||||
|
frameworks_folder := tprint("%/Contents/Frameworks", app);
|
||||||
resources_folder := tprint("%/Contents/Resources", app);
|
resources_folder := tprint("%/Contents/Resources", app);
|
||||||
|
// The executable folder is made afresh, so a renamed executable leaves nothing behind.
|
||||||
|
run_command("rm", "-rf", executable_folder);
|
||||||
make_directory_if_it_does_not_exist(executable_folder, recursive = true);
|
make_directory_if_it_does_not_exist(executable_folder, recursive = true);
|
||||||
|
make_directory_if_it_does_not_exist(frameworks_folder, recursive = true);
|
||||||
make_directory_if_it_does_not_exist(resources_folder, recursive = true);
|
make_directory_if_it_does_not_exist(resources_folder, recursive = true);
|
||||||
|
|
||||||
copied := copy_file(tprint("%/%", build_directory, executable_name), tprint("%/%", executable_folder, executable_name));
|
copied := copy_file(tprint("%/%", build_directory, executable_name), tprint("%/%", executable_folder, executable_name));
|
||||||
@ -223,23 +247,76 @@ finish_main_build :: (build: Pending_Build) {
|
|||||||
}
|
}
|
||||||
run_command("chmod", "+x", tprint("%/%", executable_folder, executable_name));
|
run_command("chmod", "+x", tprint("%/%", executable_folder, executable_name));
|
||||||
|
|
||||||
// sips makes an .icns only from a size the format holds, and icon.png may be any square size.
|
// onnxruntime, and the models, as copy_assets puts them beside the executable.
|
||||||
resized := tprint("%/icon_1024.png", build_directory);
|
ORT_LIBRARY :: "modules/onnxruntime/macos/libonnxruntime.dylib";
|
||||||
result := run_command("sips", "-z", "1024", "1024", "assets/images/icon.png", "--out", resized);
|
if file_exists(ORT_LIBRARY) clone_if_changed(ORT_LIBRARY, tprint("%/libonnxruntime.dylib", frameworks_folder));
|
||||||
if result.exit_code == 0 result = run_command("sips", "-s", "format", "icns", resized, "--out", tprint("%/%.icns", resources_folder, APP_FOLDER_NAME));
|
Model_Folder :: struct { module: string; folder: string; }
|
||||||
file_delete(resized);
|
for Model_Folder.[.{"OCR", "ocr-models"}, .{"Labels", "label-models"}, .{"Segment", "segment-models"}] {
|
||||||
if result.exit_code != 0 log_error("ERROR: Couldn't make the icon for %; the Dock will show the generic one.\n", app);
|
folder := tprint("%/%", resources_folder, it.folder);
|
||||||
|
make_directory_if_it_does_not_exist(folder);
|
||||||
|
clone_if_changed(tprint("modules/%/models.config", it.module), tprint("%/models.config", folder));
|
||||||
|
models := tprint("modules/%/models", it.module);
|
||||||
|
if file_exists(models) for file_list(models) clone_if_changed(it, tprint("%/%", folder, path_filename(it)));
|
||||||
|
}
|
||||||
|
|
||||||
if !write_entire_file(tprint("%/Contents/Info.plist", app), tprint(MACOS_INFO_PLIST, APP_NAME, APP_NAME, executable_name, APP_BUNDLE_IDENTIFIER, APP_FOLDER_NAME, VERSION, VERSION)) {
|
// An .iconset of every size, made with sips, then iconutil: sips -s format icns fails on
|
||||||
|
// macOS 27 (error 13), and icon.png may be any square size.
|
||||||
|
icon := tprint("%/%.icns", resources_folder, APP_FOLDER_NAME);
|
||||||
|
if !file_exists(icon) {
|
||||||
|
iconset := tprint("%/%.iconset", build_directory, APP_FOLDER_NAME);
|
||||||
|
run_command("rm", "-rf", iconset);
|
||||||
|
make_directory_if_it_does_not_exist(iconset);
|
||||||
|
result: Process_Result;
|
||||||
|
for size: s32.[16, 32, 128, 256, 512] {
|
||||||
|
for scale: 1..2 {
|
||||||
|
pixels := tprint("%", size * scale);
|
||||||
|
name := ifx scale == 1 then tprint("icon_%x%.png", size, size) else tprint("icon_%x%@2x.png", size, size);
|
||||||
|
result = run_command("sips", "-z", pixels, pixels, "assets/images/icon.png", "--out", tprint("%/%", iconset, name));
|
||||||
|
if result.exit_code != 0 break size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.exit_code == 0 result = run_command("iconutil", "-c", "icns", iconset, "-o", icon);
|
||||||
|
run_command("rm", "-rf", iconset);
|
||||||
|
if result.exit_code != 0 log_error("ERROR: Couldn't make the icon for %; the Dock will show the generic one.\n", app);
|
||||||
|
}
|
||||||
|
|
||||||
|
minimum := tprint("%.%", MACOS_MINIMUM_VERSION.major, MACOS_MINIMUM_VERSION.minor);
|
||||||
|
if !write_entire_file(tprint("%/Contents/Info.plist", app), tprint(MACOS_INFO_PLIST, display, display, executable_name, identifier, APP_FOLDER_NAME, VERSION, VERSION, minimum)) {
|
||||||
log_error("ERROR: Couldn't write %/Contents/Info.plist\n", app);
|
log_error("ERROR: Couldn't write %/Contents/Info.plist\n", app);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ad hoc: a signature over the whole bundle, which macOS expects, not a Developer ID.
|
log("Built % (signed %)", app, sign_macos_app_bundle(app));
|
||||||
result = run_command("codesign", "--force", "--deep", "--sign", "-", app);
|
}
|
||||||
if result.exit_code != 0 log_error("WARNING: Couldn't sign %; it may still run.\n", app);
|
|
||||||
|
|
||||||
log("Built %", app);
|
// With MACOS_SIGNING_IDENTITY when the keychain has it, and otherwise ad hoc, which is still
|
||||||
|
// a signature over the whole bundle, as macOS expects. Returns what it was signed with.
|
||||||
|
sign_macos_app_bundle :: (app: string) -> string {
|
||||||
|
result, identities := run_command("security", "find-identity", "-p", "codesigning", capture_and_return_output = true);
|
||||||
|
if result.exit_code == 0 && contains(identities, tprint("\"%\"", MACOS_SIGNING_IDENTITY)) {
|
||||||
|
result = run_command("codesign", "--force", "--deep", "--sign", MACOS_SIGNING_IDENTITY, app);
|
||||||
|
if result.exit_code == 0 return MACOS_SIGNING_IDENTITY;
|
||||||
|
log_error("WARNING: Couldn't sign % as %; signing it ad hoc instead.\n", app, MACOS_SIGNING_IDENTITY);
|
||||||
|
} else {
|
||||||
|
log("No \"%\" identity in the keychain, so % is signed ad hoc, and macOS will ask for its permissions again after every build. Ex4's scripts/macos_dev_identity.sh makes one.", MACOS_SIGNING_IDENTITY, app);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = run_command("codesign", "--force", "--deep", "--sign", "-", app);
|
||||||
|
if result.exit_code != 0 {
|
||||||
|
log_error("WARNING: Couldn't sign %; it may still run.\n", app);
|
||||||
|
return "not at all";
|
||||||
|
}
|
||||||
|
return "ad hoc";
|
||||||
|
}
|
||||||
|
|
||||||
|
// As copy_if_changed, but an APFS clone (cp -c), which shares the original's blocks.
|
||||||
|
clone_if_changed :: (from: string, to: string) {
|
||||||
|
from_time, from_size, from_ok := file_modtime_and_size(from);
|
||||||
|
to_time, to_size, to_ok := file_modtime_and_size(to);
|
||||||
|
if to_ok && from_ok && to_size == from_size && to_time >= from_time return;
|
||||||
|
file_delete(to);
|
||||||
|
result := run_command("cp", "-c", "-p", from, to);
|
||||||
|
if result.exit_code != 0 && !copy_file(from, to) log_error("Could not copy % to %.", from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
MACOS_INFO_PLIST :: #string PLIST
|
MACOS_INFO_PLIST :: #string PLIST
|
||||||
@ -257,6 +334,7 @@ finish_main_build :: (build: Pending_Build) {
|
|||||||
<key>CFBundlePackageType</key> <string>APPL</string>
|
<key>CFBundlePackageType</key> <string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key> <string>%</string>
|
<key>CFBundleShortVersionString</key> <string>%</string>
|
||||||
<key>CFBundleVersion</key> <string>%</string>
|
<key>CFBundleVersion</key> <string>%</string>
|
||||||
|
<key>LSMinimumSystemVersion</key> <string>%</string>
|
||||||
<key>NSHighResolutionCapable</key> <true/>
|
<key>NSHighResolutionCapable</key> <true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
335
macos-plan.html
335
macos-plan.html
@ -1,335 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>macOS Port Plan</title>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--bg: #fbfaf8; --surface: #ffffff; --text: #1d1d1f; --dim: #5d5d66; --line: #e3e1dc;
|
|
||||||
--accent: #3a5bd9; --accent-soft: #e9edfb; --warn: #9a5b00; --warn-soft: #fbf1e1;
|
|
||||||
--code-bg: #f2f0ec; --ok: #2e7d4f; --ok-soft: #e5f3ea;
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root:not([data-theme="light"]) {
|
|
||||||
--bg: #16171a; --surface: #1d1f23; --text: #e7e7ea; --dim: #a2a3ab; --line: #30333a;
|
|
||||||
--accent: #8aa2ff; --accent-soft: #232a45; --warn: #f0b45a; --warn-soft: #33281a;
|
|
||||||
--code-bg: #25272c; --ok: #6fcf97; --ok-soft: #1c2e24;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:root[data-theme="dark"] {
|
|
||||||
--bg: #16171a; --surface: #1d1f23; --text: #e7e7ea; --dim: #a2a3ab; --line: #30333a;
|
|
||||||
--accent: #8aa2ff; --accent-soft: #232a45; --warn: #f0b45a; --warn-soft: #33281a;
|
|
||||||
--code-bg: #25272c; --ok: #6fcf97; --ok-soft: #1c2e24;
|
|
||||||
}
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
body {
|
|
||||||
margin: 0; background: var(--bg); color: var(--text);
|
|
||||||
font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
||||||
}
|
|
||||||
main { max-width: 860px; margin: 0 auto; padding: 40px 16px 80px; }
|
|
||||||
header p { color: var(--dim); margin-top: 4px; }
|
|
||||||
h1 { font-size: 2rem; line-height: 1.2; margin: 0; }
|
|
||||||
h2 { font-size: 1.35rem; margin: 48px 0 8px; padding-top: 16px; border-top: 1px solid var(--line); }
|
|
||||||
h3 { font-size: 1.05rem; margin: 28px 0 6px; }
|
|
||||||
p, li { max-width: 72ch; }
|
|
||||||
a { color: var(--accent); }
|
|
||||||
code, pre { font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: 0.88em; }
|
|
||||||
code { background: var(--code-bg); padding: 1px 5px; border-radius: 4px; }
|
|
||||||
pre { background: var(--code-bg); padding: 12px 14px; border-radius: 8px; overflow-x: auto; line-height: 1.45; }
|
|
||||||
pre code { background: none; padding: 0; }
|
|
||||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 0.94rem; }
|
|
||||||
th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
||||||
th { color: var(--dim); font-weight: 600; }
|
|
||||||
.table-wrap { overflow-x: auto; }
|
|
||||||
.summary { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 16px 20px; margin-top: 24px; }
|
|
||||||
.summary ul { margin: 6px 0 0; padding-left: 20px; }
|
|
||||||
.note { background: var(--accent-soft); border-radius: 8px; padding: 10px 14px; margin: 14px 0; }
|
|
||||||
.risk { background: var(--warn-soft); border-radius: 8px; padding: 10px 14px; margin: 14px 0; }
|
|
||||||
.risk strong { color: var(--warn); }
|
|
||||||
.tag { display: inline-block; font-size: 0.78rem; font-weight: 600; padding: 1px 8px; border-radius: 99px; background: var(--accent-soft); color: var(--accent); }
|
|
||||||
.tag.later { background: var(--code-bg); color: var(--dim); }
|
|
||||||
.tag.decided { background: var(--ok-soft); color: var(--ok); }
|
|
||||||
nav ol { columns: 2; column-gap: 32px; padding-left: 20px; margin: 8px 0 0; }
|
|
||||||
@media (max-width: 600px) { nav ol { columns: 1; } h1 { font-size: 1.6rem; } }
|
|
||||||
.phase { border-left: 3px solid var(--accent); padding: 2px 0 2px 14px; margin: 16px 0; }
|
|
||||||
.phase h3 { margin-top: 0; }
|
|
||||||
figure { margin: 18px 0; }
|
|
||||||
figcaption { color: var(--dim); font-size: 0.88rem; margin-top: 6px; }
|
|
||||||
svg text { fill: var(--text); font: 13px system-ui, sans-serif; }
|
|
||||||
svg .dim { fill: var(--dim); }
|
|
||||||
svg .box { fill: var(--surface); stroke: var(--line); }
|
|
||||||
svg .hl { fill: var(--accent-soft); stroke: var(--accent); }
|
|
||||||
svg .arrow { stroke: var(--dim); fill: none; marker-end: url(#a); }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<h1>mcap on macOS: building again, then feature parity</h1>
|
|
||||||
<p>Planning document · 2026-10-10 · for mcap after 0.1.11 · <strong>not started</strong></p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="summary">
|
|
||||||
<strong>In short</strong>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Where it stands:</strong> mcap was built to compile on macOS (Apple silicon, SDL3 and Metal), but it hasn't been built on a Mac since at least 0.0.8. Two build breaks were found and fixed in 0.1.11 without a Mac. Everything added since the template is Windows first, and much of it Windows only.</li>
|
|
||||||
<li><span class="tag decided">Proposed</span> <strong>Eight phases</strong>, smallest risk first:
|
|
||||||
<ol>
|
|
||||||
<li>Build and run on a Mac.</li>
|
|
||||||
<li>Fill the small platform gaps: choosing a folder, the Trash, the clipboard, the temporary folder.</li>
|
|
||||||
<li>Images through ImageIO.</li>
|
|
||||||
<li>Video through FFmpeg for macOS.</li>
|
|
||||||
<li>The models on the GPU through CoreML.</li>
|
|
||||||
<li>Screen capture stills.</li>
|
|
||||||
<li>Screen recording.</li>
|
|
||||||
<li>A signed release.</li>
|
|
||||||
</ol></li>
|
|
||||||
<li><span class="tag decided">Proposed</span> <strong>One codebase, the same file formats.</strong> An <code>.mcap</code> made on either system opens on the other. Where Windows uses WIC, DXGI and WASAPI, macOS uses ImageIO and ScreenCaptureKit. FFmpeg, onnxruntime, the encoder, the remux and the mixer stay shared.</li>
|
|
||||||
<li><span class="tag decided">Proposed</span> <strong>ScreenCaptureKit through a small Objective-C shim.</strong> It is asynchronous: completion blocks, delegates, dispatch queues. A few hundred lines of Objective-C, compiled with clang into a static library with a plain C interface, is far safer than doing that from Jai. Open question 6.</li>
|
|
||||||
<li><strong>The big unknowns</strong> need a Mac to settle. Which macOS to require (14 Sonoma is proposed). Signing: macOS remembers the Screen Recording permission per signature, and an ad hoc signature changes with every build. What the capture keys should be: Mac keyboards have no PrintScreen. See <a href="#questions">open questions</a>.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<ol>
|
|
||||||
<li><a href="#today">Where it stands</a></li>
|
|
||||||
<li><a href="#gaps">What is Windows only</a></li>
|
|
||||||
<li><a href="#approach">Approach</a></li>
|
|
||||||
<li><a href="#capture">Screen capture on macOS</a></li>
|
|
||||||
<li><a href="#permissions">Permissions and signing</a></li>
|
|
||||||
<li><a href="#phases">Phases</a></li>
|
|
||||||
<li><a href="#risks">Risks</a></li>
|
|
||||||
<li><a href="#questions">Open questions</a></li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<h2 id="today">1. Where it stands</h2>
|
|
||||||
<p>From the code; none of this has been run on a Mac in this project's history.</p>
|
|
||||||
<div class="table-wrap"><table>
|
|
||||||
<tr><th>Part</th><th>macOS today</th></tr>
|
|
||||||
<tr><td>Window, ImGui, themes, fonts</td><td>SDL3 with SDL_GPU on Metal (<code>src/backends/sdl3_gpu3.jai</code>). The menu-bar tray is SDL's. Fonts and CJK fallbacks have macOS paths (<code>platform_macos.jai</code>).</td></tr>
|
|
||||||
<tr><td>Build</td><td><code>jai first.jai</code> makes <code>bin/mcap-macos-debug</code>, and with <code>- release</code> <code>bin/mcap.app</code>, signed ad hoc. Apple silicon only, with a native <code>jai</code>. The <code>.app</code> holds only the executable and <code>libSDL3.0.dylib</code>. onnxruntime and the models are copied beside it, not into it.</td></tr>
|
|
||||||
<tr><td>Type check from Windows</td><td>Not possible: <code>jai check_targets.jai - macos</code> runs only on a Mac, because the Objective-C module runs code at compile time.</td></tr>
|
|
||||||
<tr><td>Known breaks, fixed in 0.1.11</td><td>
|
|
||||||
<ul>
|
|
||||||
<li><code>unix_sdl3_gpu3.jai</code> pulled all of ImGui into the app's scope, so ImGui's <code>Image</code> clashed with Image_Decode's (broken since 0.0.8).</li>
|
|
||||||
<li>A Windows <code>RECT</code> was in the shared <code>App</code> struct (since 0.1.6).</li>
|
|
||||||
</ul>
|
|
||||||
The Linux check passes after both, and macOS shares the same files.</td></tr>
|
|
||||||
<tr><td>Library location</td><td>Hard-coded as <code>D:/McapIndex/mcap</code> (<code>VIEWER_FOLDER</code>). On a Mac only <code>-library</code> works.</td></tr>
|
|
||||||
<tr><td>Global hotkeys</td><td>Carbon <code>RegisterEventHotKey</code> (<code>macos_global_hotkeys.jai</code>). <code>capture_region</code> and <code>record_region</code> default to <code>none</code>.</td></tr>
|
|
||||||
<tr><td>OCR, labels, segmentation</td><td>onnxruntime 1.30.0 for macOS arm64 is in <code>fetch.jai</code>. Sessions use CUDA or the CPU (<code>session.jai</code>), so on a Mac it's CPU only, several times slower than a GPU.</td></tr>
|
|
||||||
</table></div>
|
|
||||||
|
|
||||||
<h2 id="gaps">2. What is Windows only</h2>
|
|
||||||
<div class="table-wrap"><table>
|
|
||||||
<tr><th>Feature</th><th>Windows</th><th>macOS today</th><th>macOS plan</th></tr>
|
|
||||||
<tr><td>Images other than PNG, JPEG, GIF, BMP, PSD, TGA</td><td>WIC (<code>Image_Decode/wic.jai</code>)</td><td>Refused</td><td>ImageIO, <code>CGImageSource</code> (phase 3)</td></tr>
|
|
||||||
<tr><td>Videos: import, frame for the thumbnail</td><td>FFmpeg (<code>modules/ffmpeg</code>, Windows bindings only)</td><td>Skipped</td><td>FFmpeg for macOS (phase 4)</td></tr>
|
|
||||||
<tr><td>Import: choosing a folder</td><td>Explorer's picker (<code>windows_choose_folder</code>)</td><td>Typed path only</td><td><code>NSOpenPanel</code> (phase 2)</td></tr>
|
|
||||||
<tr><td>Delete to the bin</td><td>Recycle Bin (<code>SHFileOperationW</code>)</td><td>A "deleted" folder beside the file</td><td><code>NSFileManager trashItemAtURL</code> (phase 2)</td></tr>
|
|
||||||
<tr><td>Temporary folder (open original, recordings)</td><td><code>%TEMP%</code></td><td><code>/tmp</code></td><td><code>NSTemporaryDirectory()</code> (phase 2)</td></tr>
|
|
||||||
<tr><td>Image to the clipboard</td><td><code>CF_DIB</code></td><td>None</td><td><code>NSPasteboard</code>, PNG (phase 2)</td></tr>
|
|
||||||
<tr><td>Models on the GPU</td><td>CUDA</td><td>CPU</td><td>CoreML execution provider (phase 5)</td></tr>
|
|
||||||
<tr><td>Screen capture stills</td><td>GDI overlay, <code>BitBlt</code></td><td>None</td><td>Cocoa overlay, ScreenCaptureKit screenshots (phase 6)</td></tr>
|
|
||||||
<tr><td>Screen recording, border, toolbar</td><td>DXGI or Windows.Graphics.Capture, GDI windows</td><td>None</td><td>ScreenCaptureKit stream, Cocoa panels (phase 7)</td></tr>
|
|
||||||
<tr><td>Sound</td><td>WASAPI</td><td>None</td><td>ScreenCaptureKit audio, plus its microphone or AVAudioEngine (phase 7)</td></tr>
|
|
||||||
<tr><td>Release</td><td><code>release.jai</code>: a zip with CUDA</td><td>None</td><td>A signed <code>.app</code> in a <code>.dmg</code> (phase 8)</td></tr>
|
|
||||||
</table></div>
|
|
||||||
<p>Already shared and fine as they are:</p>
|
|
||||||
<ul>
|
|
||||||
<li>The <code>.mcap</code> format, catalog, search, Stats.</li>
|
|
||||||
<li><code>.snagx</code> import, EXIF (<code>exif.jai</code> reads it itself).</li>
|
|
||||||
<li>The settings and hotkey files.</li>
|
|
||||||
<li>The encoder, remux and mixer in <code>modules/Screen_Record</code>. Their capture sources aren't shared.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2 id="approach">3. Approach</h2>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Same interfaces, a second implementation.</strong> Each Windows-only piece already sits behind a function the app calls. The macOS version goes beside it, in its own file:
|
|
||||||
<ul>
|
|
||||||
<li><code>platform_screen_capture_select</code>, <code>platform_copy_image_to_clipboard</code>.</li>
|
|
||||||
<li><code>image_decode</code>'s WIC branch.</li>
|
|
||||||
<li><code>video_poster</code>.</li>
|
|
||||||
<li><code>Screen_Record</code>'s source: a third source beside <code>dxgi_source</code> and <code>wgc_source</code>.</li>
|
|
||||||
</ul>
|
|
||||||
The app's own files (<code>screen_capture.jai</code>, <code>screen_record.jai</code>, <code>import.jai</code>) shouldn't need to know which system they're on beyond what they do now.</li>
|
|
||||||
<li><strong>FFmpeg on macOS too</strong>, rather than AVFoundation. Video import, the thumbnail frame, the recording encoder and the remux are FFmpeg code already. On a Mac the encoder would be <code>h264_videotoolbox</code>, Apple's hardware encoder, put first in <code>RECORD_ENCODERS</code>. The cost is shipping FFmpeg's dylibs in the <code>.app</code>, and bindings for their macOS names.</li>
|
|
||||||
<li><strong>The Objective-C shim</strong> (<code>modules/Screen_Record/macos/capture.m</code>) holds ScreenCaptureKit and nothing else. It has a C interface:
|
|
||||||
<ul>
|
|
||||||
<li><code>mcap_sck_screenshot</code></li>
|
|
||||||
<li><code>mcap_sck_stream_start</code></li>
|
|
||||||
<li><code>mcap_sck_next_frame</code></li>
|
|
||||||
<li><code>mcap_sck_next_audio</code></li>
|
|
||||||
<li><code>mcap_sck_stream_stop</code></li>
|
|
||||||
</ul>
|
|
||||||
It is compiled by <code>first.jai</code> with clang when building on a Mac, and linked statically. AppKit work that is plain message sends (<code>NSOpenPanel</code>, <code>NSPasteboard</code>, the overlay and toolbar windows) stays in Jai through the Objective-C module the app already uses.</li>
|
|
||||||
<li><strong>The build is checked on every phase</strong> with <code>jai check_targets.jai - macos</code> and the test suites on the Mac. <code>jai check_targets.jai - linux</code> on Windows catches the shared files between Mac sessions.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2 id="capture">4. Screen capture on macOS</h2>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Stills:</strong> the moment the key is pressed, each screen is copied:
|
|
||||||
<ul>
|
|
||||||
<li>Copied with <code>SCScreenshotManager captureImageWithFilter</code>, on macOS 14 and later. <code>CGWindowListCreateImage</code> is deprecated in macOS 15.</li>
|
|
||||||
<li>Shown frozen in a borderless <code>NSWindow</code> per screen, at the screen-saver level. A window can't span screens on macOS as the one Windows overlay does.</li>
|
|
||||||
<li>The drag, magnifier and keys work as on Windows. The overlay's drawing (darkened copy, bright selection, outline, magnifier) becomes Core Graphics in each window's <code>drawRect</code>, or an <code>NSImage</code> drawn by the shim.</li>
|
|
||||||
</ul></li>
|
|
||||||
<li><strong>Points and pixels:</strong> AppKit works in points. A Retina screen has 2 pixels a point, and screens can differ. The region is chosen in points and converted to each screen's pixels for the capture. The 240 × 180 minimum is in pixels, as on Windows.</li>
|
|
||||||
<li><strong>Recording:</strong> an <code>SCStream</code> for the region's display (<code>sourceRect</code>) delivers BGRA <code>CVPixelBuffer</code>s with timestamps on the host clock.
|
|
||||||
<ul>
|
|
||||||
<li>The shim hands them over as the pipeline's frames, and the encoder and the rest are unchanged.</li>
|
|
||||||
<li>The cursor: <code>showsCursor</code>.</li>
|
|
||||||
<li>Keeping mcap's own border and toolbar out: an <code>SCContentFilter</code> that excludes the app's windows. That's cleaner than Windows' window flag, and doesn't depend on it.</li>
|
|
||||||
</ul></li>
|
|
||||||
<li><strong>Sound:</strong>
|
|
||||||
<ul>
|
|
||||||
<li>System sound comes from the same stream (<code>capturesAudio</code>, macOS 13 on), as 48 kHz float.</li>
|
|
||||||
<li>The microphone comes from the stream too on macOS 15 (<code>captureMicrophone</code>), else from <code>AVAudioEngine</code>'s input.</li>
|
|
||||||
<li>Both go into the mixer as packets with host-clock times, as WASAPI's do.</li>
|
|
||||||
</ul></li>
|
|
||||||
<li><strong>The border and toolbar:</strong> non-activating <code>NSPanel</code>s at the floating level. The toolbar's buttons ask, and the app acts on its next frame, as on Windows.</li>
|
|
||||||
<li><strong>The window it was taken from:</strong> the frontmost app comes from <code>NSWorkspace</code>, and window titles from <code>CGWindowListCopyWindowInfo</code>. The titles need the Screen Recording permission, which capture needs anyway.</li>
|
|
||||||
<li><strong>Hotkeys:</strong> no PrintScreen on Mac keyboards. ⌘⇧3, 4 and 5 are the system's screenshot keys. See open question 3.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2 id="permissions">5. Permissions and signing</h2>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Screen Recording</strong> (Privacy & Security) must be granted before any capture. The first capture asks; until it is granted, the overlay gets only the desktop, without other apps' windows. mcap will check (<code>CGPreflightScreenCaptureAccess</code>) and say what to do instead of showing an empty capture.</li>
|
|
||||||
<li><strong>Microphone</strong> permission is asked the first time the Mic button is turned on. <code>Info.plist</code> gains <code>NSMicrophoneUsageDescription</code>.</li>
|
|
||||||
<li><strong>macOS remembers these per code signature.</strong> The release <code>.app</code> is signed ad hoc today, and an ad hoc signature changes with every build, so each build would ask again, or silently stay refused. A stable signing identity solves it: an Apple Development certificate for builds, and a Developer ID for releases, notarized so Gatekeeper opens it. See open question 4.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2 id="phases">6. Phases</h2>
|
|
||||||
<p>Each phase ends with something to run and check on the Mac, and is a commit. They are ordered so a usable mcap comes first: by the end of phase 4 the library browser, search, imports and OCR all work.</p>
|
|
||||||
|
|
||||||
<div class="phase">
|
|
||||||
<h3>Phase 1: Build and run on a Mac</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>The type check:</strong> <code>jai check_targets.jai - macos</code>, fixing what it finds.</li>
|
|
||||||
<li><strong>The builds:</strong> <code>jai first.jai</code> and <code>- release</code>.</li>
|
|
||||||
<li><strong>A library folder for macOS:</strong> a default in place of <code>D:/McapIndex/mcap</code>, open question 5.</li>
|
|
||||||
<li><strong>The <code>.app</code> made whole:</strong> <code>libonnxruntime.dylib</code> in <code>Contents/Frameworks</code>, and the OCR, label and segmentation models in <code>Contents/Resources</code>, found from there.</li>
|
|
||||||
<li><strong>Tests:</strong> <code>run_tests -headless</code> on the Mac.</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Done when:</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li>Both builds succeed and the test suites pass on the Mac.</li>
|
|
||||||
<li><code>mcap.app</code>, started from Finder, shows a library copied from Windows: thumbnails, the Capture window, search, Stats.</li>
|
|
||||||
<li>An OCR scan reads captures on the CPU, and its time per capture is recorded.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="phase">
|
|
||||||
<h3>Phase 2: The small platform gaps</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Choosing a folder:</strong> <code>NSOpenPanel</code> for Import's Browse button.</li>
|
|
||||||
<li><strong>Delete:</strong> into the Trash with <code>NSFileManager trashItemAtURL</code>, with "Move to Trash" in the delete dialog.</li>
|
|
||||||
<li><strong>Temporary files:</strong> <code>NSTemporaryDirectory()</code> for opening an original and for recordings.</li>
|
|
||||||
<li><strong>The clipboard:</strong> <code>platform_copy_image_to_clipboard</code> through <code>NSPasteboard</code>, as PNG and TIFF.</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Done when:</strong> Browse picks a folder; a deleted capture is in the Trash and can be put back from there; Open plays a video's original; an image pasted from the clipboard (phase 6 is its first user, so a test fills it) pastes into Preview.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="phase">
|
|
||||||
<h3>Phase 3: Images through ImageIO</h3>
|
|
||||||
<p><code>Image_Decode/imageio.jai</code>: <code>CGImageSourceCreateWithData</code> and <code>CGImageSourceCreateImageAtIndex</code>, drawn into an RGBA bitmap context. It takes the formats WIC takes on Windows:</p>
|
|
||||||
<ul>
|
|
||||||
<li>HEIC and AVIF (AVIF from macOS 13).</li>
|
|
||||||
<li>WebP (from macOS 11).</li>
|
|
||||||
<li>TIFF.</li>
|
|
||||||
<li>The camera RAW formats.</li>
|
|
||||||
</ul>
|
|
||||||
<p>EXIF orientation stays <code>exif.jai</code>'s. ImageIO is asked for the image as stored, so it isn't turned twice.</p>
|
|
||||||
<p><strong>Done when:</strong> <code>image-probe</code> on the Mac decodes the same test images as on Windows, the same way up and the same size, and importing an iPhone photo folder makes the same <code>.mcap</code> files, except for the import date.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="phase">
|
|
||||||
<h3>Phase 4: Video through FFmpeg for macOS</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>FFmpeg itself:</strong> a shared FFmpeg 6 build for macOS arm64, with VideoToolbox and the native AAC encoder. Its dylibs and their SHA-256 sums go in <code>modules/ffmpeg/fetch.jai</code>, as onnxruntime's are.</li>
|
|
||||||
<li><strong>Bindings:</strong> <code>modules/ffmpeg/macos.jai</code>, from the same headers with the dylib names, and <code>#load</code>ed on macOS.</li>
|
|
||||||
<li><strong>The video code:</strong> <code>video_ffmpeg.jai</code> and the import's video path, no longer Windows only.</li>
|
|
||||||
<li><strong>The <code>.app</code>:</strong> the dylibs go in it.</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Done when:</strong> importing a folder with MP4, MOV and HEVC iPhone videos gives the same <code>.mcap</code> files as on Windows, with thumbnails, and they play with Open.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="phase">
|
|
||||||
<h3>Phase 5: The models on the GPU, with CoreML</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>CoreML first:</strong> <code>session.jai</code> asks onnxruntime for the CoreML execution provider on macOS (<code>SessionOptionsAppendExecutionProvider</code> with <code>"CoreML"</code>, the ML Program format), and the CPU for what it can't take.</li>
|
|
||||||
<li><strong>Measured per model:</strong> PP-OCR detection and recognition, SigLIP 2, Mask2Former. Each is timed against the CPU and checked to give the same output as on Windows, within the tolerances the Windows probes already use.</li>
|
|
||||||
<li><strong>The status line</strong> says CoreML, as it says CUDA on Windows.</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Done when:</strong> OCR, labels and segmentation give the same results as on Windows on the probes' test images, and each is faster than the CPU, or is left on the CPU with the reason recorded.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="phase">
|
|
||||||
<h3>Phase 6: Screen capture stills</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>The shim's screenshot</strong> (<code>mcap_sck_screenshot</code>, per display).</li>
|
|
||||||
<li><strong>The overlay:</strong> a window per screen, with the drag, magnifier, keys, and a click for the whole screen.</li>
|
|
||||||
<li><strong>The window it came from:</strong> its title and app.</li>
|
|
||||||
<li><strong>The clipboard</strong> (phase 2).</li>
|
|
||||||
<li><strong>The capture hotkey</strong> (open question 3).</li>
|
|
||||||
<li><strong>Permission:</strong> the check, and what mcap says when Screen Recording isn't granted.</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Done when:</strong> on a Mac with a Retina screen and an external non-Retina one:</p>
|
|
||||||
<ul>
|
|
||||||
<li>The hotkey freezes both screens.</li>
|
|
||||||
<li>A region dragged on either is taken at the screen's own pixels, and is on the clipboard and in Screen Captures with its window's title and app.</li>
|
|
||||||
<li>With the permission refused, mcap says how to grant it.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="phase">
|
|
||||||
<h3>Phase 7: Screen recording</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>The source:</strong> <code>modules/Screen_Record/sck_source.jai</code> over the shim's stream, as the third source. The encoder tries <code>h264_videotoolbox</code> first.</li>
|
|
||||||
<li><strong>Sound:</strong> system sound and the microphone, through the existing mixer.</li>
|
|
||||||
<li><strong>The overlay's video mode</strong>, with the 240 × 180 minimum.</li>
|
|
||||||
<li><strong>The border and toolbar</strong> as panels, kept out of the stream by the content filter. Pause, Discard, the sound buttons, and leftovers after a crash.</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Done when</strong> the same checks pass as on Windows:</p>
|
|
||||||
<ul>
|
|
||||||
<li><code>record-probe</code> runs on the Mac, with <code>-sync-test</code>, <code>-pause</code> and <code>-mic-toggle</code>.</li>
|
|
||||||
<li>Neither border nor toolbar is in a recording.</li>
|
|
||||||
<li>A 10 s pause leaves no gap.</li>
|
|
||||||
<li>Beeps stay within about 25 ms of their flashes with no drift over 3 minutes.</li>
|
|
||||||
<li>A killed recording is offered at the next start.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="phase">
|
|
||||||
<h3>Phase 8: A signed release</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong><code>release.jai</code> for macOS:</strong> the <code>.app</code> with everything in it (SDL3, onnxruntime, FFmpeg, the models), signed with a Developer ID with the hardened runtime and the entitlements it needs.</li>
|
|
||||||
<li><strong>Notarized and stapled.</strong></li>
|
|
||||||
<li><strong>Packaged</strong> in a <code>.dmg</code>, with its SHA-256 printed, as the Windows zip's is.</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Done when:</strong> the <code>.dmg</code>, downloaded on a second Mac, opens without a Gatekeeper warning, asks once for Screen Recording, and keeps that permission across an update to the next release.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 id="risks">7. Risks</h2>
|
|
||||||
<div class="risk"><strong>No Mac in this loop yet.</strong> Everything here can be written on Windows, but only built, run and checked on a Mac. Each phase needs a session there, yours or a remote one (open question 1). The Linux type check catches the shared files in between, but not the Objective-C or the macOS-only files.</div>
|
|
||||||
<div class="risk"><strong>CoreML and the models.</strong> CoreML takes some operators and not others. Mask2Former's attention and the panoptic post-processing may fall back to the CPU in parts, which can be slower than the CPU alone, as the data moves back and forth. Phase 5 measures each model, and keeps the CPU where CoreML doesn't win.</div>
|
|
||||||
<div class="risk"><strong>Permissions and signing.</strong> Without a stable signature, Screen Recording has to be granted again after every build, and a refused permission gives a capture of the empty desktop rather than an error. Development needs a certificate early, by phase 6.</div>
|
|
||||||
<div class="risk"><strong>FFmpeg licensing.</strong> The Windows FFmpeg is a GPL build (with libx264). On macOS an LGPL build is enough: VideoToolbox does H.264, and FFmpeg's own AAC encoder does sound. That avoids GPL obligations for the Mac release; the Windows release should be checked against the same question.</div>
|
|
||||||
<div class="risk"><strong>Multi-screen overlays.</strong> One window per screen, with Spaces, full-screen apps and screens at different scales, is where macOS differs most from Windows. The overlay must sit above full-screen apps (<code>NSWindowCollectionBehaviorCanJoinAllSpaces</code> and <code>FullScreenAuxiliary</code>).</div>
|
|
||||||
|
|
||||||
<h2 id="questions">8. Open questions</h2>
|
|
||||||
<ol>
|
|
||||||
<li><strong>The Mac:</strong> which Mac and macOS version will the work be checked on? Can I build and run there (SSH, or a remote session), or will you run the builds and send back the output?</li>
|
|
||||||
<li><strong>The oldest macOS:</strong> 14 Sonoma, for ScreenCaptureKit's screenshots, is proposed. 13 would need the deprecated <code>CGWindowListCreateImage</code> for stills; 15 would give the microphone in the stream itself.</li>
|
|
||||||
<li><strong>The keys:</strong> Mac keyboards have no PrintScreen, and ⌘⇧3/4/5 are the system's. Proposed: <strong>⌘⇧2</strong> for a still and <strong>⌃⌘⇧2</strong> to record, or F13 (PrintScreen on Apple's full keyboards) and ⌃F13, or your own.</li>
|
|
||||||
<li><strong>Signing:</strong> do you have, or want, an Apple Developer account (99 USD a year) for a stable development signature now and Developer ID notarization for releases? Without it, phases 6 and 7 work but ask for permission after every build.</li>
|
|
||||||
<li><strong>The library on a Mac:</strong> where should it default to? Proposed: <code>~/Pictures/mcap</code>. Should the library folder become a setting on both systems, in place of the hard-coded <code>D:/McapIndex/mcap</code>?</li>
|
|
||||||
<li><strong>The shim:</strong> is a small Objective-C file built with clang acceptable in the repo, or should ScreenCaptureKit be done from Jai alone, at more cost and risk?</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -52,7 +52,8 @@ sha256_of :: (path: string) -> string {
|
|||||||
if lines.count < 2 return "";
|
if lines.count < 2 return "";
|
||||||
return to_lower_copy(replace(trim(lines[1]), " ", "")); // line 2 is the hash
|
return to_lower_copy(replace(trim(lines[1]), " ", "")); // line 2 is the hash
|
||||||
} else {
|
} else {
|
||||||
command := ifx OS == .MACOS then string.["shasum", "-a", "256", path] else string.["sha256sum", path];
|
#if OS == .MACOS command := string.["shasum", "-a", "256", path];
|
||||||
|
else command := string.["sha256sum", path];
|
||||||
result, output := run_command(..command, capture_and_return_output = true);
|
result, output := run_command(..command, capture_and_return_output = true);
|
||||||
if result.exit_code != 0 return "";
|
if result.exit_code != 0 return "";
|
||||||
return to_lower_copy(trim(split(output, " ")[0]));
|
return to_lower_copy(trim(split(output, " ")[0]));
|
||||||
|
|||||||
@ -59,7 +59,8 @@ sha256_of :: (path: string) -> string {
|
|||||||
if lines.count < 2 return "";
|
if lines.count < 2 return "";
|
||||||
return to_lower_copy(replace(trim(lines[1]), " ", "")); // line 2 is the hash
|
return to_lower_copy(replace(trim(lines[1]), " ", "")); // line 2 is the hash
|
||||||
} else {
|
} else {
|
||||||
command := ifx OS == .MACOS then string.["shasum", "-a", "256", path] else string.["sha256sum", path];
|
#if OS == .MACOS command := string.["shasum", "-a", "256", path];
|
||||||
|
else command := string.["sha256sum", path];
|
||||||
result, output := run_command(..command, capture_and_return_output = true);
|
result, output := run_command(..command, capture_and_return_output = true);
|
||||||
if result.exit_code != 0 return "";
|
if result.exit_code != 0 return "";
|
||||||
return to_lower_copy(trim(split(output, " ")[0]));
|
return to_lower_copy(trim(split(output, " ")[0]));
|
||||||
|
|||||||
@ -113,7 +113,8 @@ sha256_of :: (path: string) -> string {
|
|||||||
if lines.count < 2 return "";
|
if lines.count < 2 return "";
|
||||||
return to_lower_copy(replace(trim(lines[1]), " ", "")); // line 2 is the hash
|
return to_lower_copy(replace(trim(lines[1]), " ", "")); // line 2 is the hash
|
||||||
} else {
|
} else {
|
||||||
command := ifx OS == .MACOS then string.["shasum", "-a", "256", path] else string.["sha256sum", path];
|
#if OS == .MACOS command := string.["shasum", "-a", "256", path];
|
||||||
|
else command := string.["sha256sum", path];
|
||||||
result, output := run_command(..command, capture_and_return_output = true);
|
result, output := run_command(..command, capture_and_return_output = true);
|
||||||
if result.exit_code != 0 return "";
|
if result.exit_code != 0 return "";
|
||||||
return to_lower_copy(trim(split(output, " ")[0]));
|
return to_lower_copy(trim(split(output, " ")[0]));
|
||||||
|
|||||||
90
modules/onnxruntime/include/coreml_provider_factory.h
Normal file
90
modules/onnxruntime/include/coreml_provider_factory.h
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "onnxruntime_c_api.h"
|
||||||
|
|
||||||
|
// COREMLFlags are bool options we want to set for CoreML EP
|
||||||
|
// This enum is defined as bit flags, and cannot have negative value
|
||||||
|
// To generate an uint32_t coreml_flags for using with OrtSessionOptionsAppendExecutionProvider_CoreML below,
|
||||||
|
// uint32_t coreml_flags = 0;
|
||||||
|
// coreml_flags |= COREML_FLAG_USE_CPU_ONLY;
|
||||||
|
enum COREMLFlags {
|
||||||
|
COREML_FLAG_USE_NONE = 0x000,
|
||||||
|
|
||||||
|
// Using CPU only in CoreML EP, this may decrease the perf but will provide
|
||||||
|
// reference output value without precision loss, which is useful for validation
|
||||||
|
COREML_FLAG_USE_CPU_ONLY = 0x001,
|
||||||
|
|
||||||
|
// Enable CoreML EP on subgraph
|
||||||
|
COREML_FLAG_ENABLE_ON_SUBGRAPH = 0x002,
|
||||||
|
|
||||||
|
// By default CoreML Execution provider will be enabled for all compatible Apple devices
|
||||||
|
// Enable this option will only enable CoreML EP for Apple devices with ANE (Apple Neural Engine)
|
||||||
|
// Please note, enable this option does not guarantee the entire model to be executed using ANE only
|
||||||
|
COREML_FLAG_ONLY_ENABLE_DEVICE_WITH_ANE = 0x004,
|
||||||
|
|
||||||
|
// Only allow CoreML EP to take nodes with inputs with static shapes. By default it will also allow inputs with
|
||||||
|
// dynamic shapes. However, the performance may be negatively impacted if inputs have dynamic shapes.
|
||||||
|
COREML_FLAG_ONLY_ALLOW_STATIC_INPUT_SHAPES = 0x008,
|
||||||
|
|
||||||
|
// Create an MLProgram. By default it will create a NeuralNetwork model. Requires Core ML 5 or later.
|
||||||
|
COREML_FLAG_CREATE_MLPROGRAM = 0x010,
|
||||||
|
|
||||||
|
// https://developer.apple.com/documentation/coreml/mlcomputeunits?language=objc
|
||||||
|
// there are four compute units:
|
||||||
|
// MLComputeUnitsCPUAndNeuralEngine|MLComputeUnitsCPUAndGPU|MLComputeUnitsCPUOnly|MLComputeUnitsAll
|
||||||
|
// different CU will have different performance and power consumption
|
||||||
|
COREML_FLAG_USE_CPU_AND_GPU = 0x020,
|
||||||
|
// Keep COREML_FLAG_LAST at the end of the enum definition
|
||||||
|
// And assign the last COREMLFlag to it
|
||||||
|
COREML_FLAG_LAST = COREML_FLAG_USE_CPU_AND_GPU,
|
||||||
|
};
|
||||||
|
|
||||||
|
// MLComputeUnits can be one of the following values:
|
||||||
|
// 'MLComputeUnitsCPUAndNeuralEngine|MLComputeUnitsCPUAndGPU|MLComputeUnitsCPUOnly|MLComputeUnitsAll'
|
||||||
|
// these values are intended to be used with Ort::SessionOptions::AppendExecutionProvider (C++ API)
|
||||||
|
// and SessionOptionsAppendExecutionProvider (C API). For the old API, use COREMLFlags instead.
|
||||||
|
static const char* const kCoremlProviderOption_MLComputeUnits = "MLComputeUnits";
|
||||||
|
static const char* const kCoremlProviderOption_ModelFormat = "ModelFormat";
|
||||||
|
// same as COREML_FLAG_ONLY_ALLOW_STATIC_INPUT_SHAPES
|
||||||
|
static const char* const kCoremlProviderOption_RequireStaticInputShapes = "RequireStaticInputShapes";
|
||||||
|
static const char* const kCoremlProviderOption_EnableOnSubgraphs = "EnableOnSubgraphs";
|
||||||
|
// provided by https://developer.apple.com/documentation/coreml/mloptimizationhints-swift.struct/specializationstrategy-swift.property
|
||||||
|
// Core ML segments the model’s compute graph and specializes each segment for the target compute device.
|
||||||
|
// This process can affect the model loading time and the prediction latency.
|
||||||
|
// Use this option to tailor the specialization strategy for your model.
|
||||||
|
static const char* const kCoremlProviderOption_SpecializationStrategy = "SpecializationStrategy";
|
||||||
|
// Profile the Core ML MLComputePlan.
|
||||||
|
// This logs the hardware each operator is dispatched to and the estimated execution time.
|
||||||
|
// Intended for developer usage but provide useful diagnostic information if performance is not as expected.
|
||||||
|
static const char* const kCoremlProviderOption_ProfileComputePlan = "ProfileComputePlan";
|
||||||
|
// please refer to https://developer.apple.com/documentation/coreml/mlmodelconfiguration/allowlowprecisionaccumulationongpu
|
||||||
|
static const char* const kCoremlProviderOption_AllowLowPrecisionAccumulationOnGPU = "AllowLowPrecisionAccumulationOnGPU";
|
||||||
|
// Specify the directory to cache any CoreML models created from the ONNX model in.
|
||||||
|
// CoreML EP will convert onnx subgraph to CoreML model and save to disk.
|
||||||
|
// If this path is not specified, the model will be saved to a temp directory and deleted after the session is closed.
|
||||||
|
// otherwise, the model will be saved to the specified path and User should manage to delete the model.
|
||||||
|
|
||||||
|
// we do NOT detect if the onnx model has changed and no longer matches the cached model.
|
||||||
|
// the user should carefully manage the cache if modifying/replacing a model.
|
||||||
|
// The cache key is generated by
|
||||||
|
// 1. User provided key in metadata_props if found (preferred)
|
||||||
|
// 2. Hash of the model url the inference session was created with
|
||||||
|
// 3. Hash of the input/output names of the model
|
||||||
|
// Please find out how to set metadata_props in the onnxruntime API documentation. https://onnxruntime.ai/docs/execution-providers/CoreML-ExecutionProvider.html#configuration-options
|
||||||
|
static const char* const kCoremlProviderOption_ModelCacheDirectory = "ModelCacheDirectory";
|
||||||
|
|
||||||
|
// User provided cache-key in metadata_props.
|
||||||
|
static const char* const kCOREML_CACHE_KEY = "COREML_CACHE_KEY";
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ORT_EXPORT ORT_API_STATUS(OrtSessionOptionsAppendExecutionProvider_CoreML,
|
||||||
|
_In_ OrtSessionOptions* options, uint32_t coreml_flags);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
14
modules/onnxruntime/include/webgpu_provider_factory.h
Normal file
14
modules/onnxruntime/include/webgpu_provider_factory.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
// Dummy file to provide a signal in the ONNX Runtime C cocoapod as to whether the WebGPU EP was included in the build.
|
||||||
|
// If it was, this file will be included in the cocoapod, and a test like this can be used:
|
||||||
|
//
|
||||||
|
// #if __has_include(<onnxruntime/webgpu_provider_factory.h>)
|
||||||
|
// #define WEBGPU_EP_AVAILABLE 1
|
||||||
|
// #else
|
||||||
|
// #define WEBGPU_EP_AVAILABLE 0
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// The WebGPU EP can be enabled via the generic SessionOptionsAppendExecutionProvider method, so no direct usage of
|
||||||
|
// the provider factory is required.
|
||||||
@ -14,15 +14,17 @@ ort: struct {
|
|||||||
log_level := OrtLoggingLevel.ORT_LOGGING_LEVEL_WARNING;
|
log_level := OrtLoggingLevel.ORT_LOGGING_LEVEL_WARNING;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads onnxruntime from the first folder that has it: beside the running executable, then each
|
// Loads onnxruntime from the first folder that has it: beside the running executable (or in a macOS
|
||||||
// of folders. A full path is always used: Windows keeps an older onnxruntime.dll in System32 for
|
// bundle's Contents/Frameworks), then each of folders. A full path is always used: Windows keeps an
|
||||||
// its own use, which a bare name could find first.
|
// older onnxruntime.dll in System32 for its own use, which a bare name could find first.
|
||||||
ort_load :: (folders: [] string = .[]) -> ok: bool, error: string {
|
ort_load :: (folders: [] string = .[]) -> ok: bool, error: string {
|
||||||
if ort.api return true, "";
|
if ort.api return true, "";
|
||||||
|
|
||||||
candidates: [..] string;
|
candidates: [..] string;
|
||||||
candidates.allocator = temp;
|
candidates.allocator = temp;
|
||||||
array_add(*candidates, path_strip_filename(get_path_of_running_executable()));
|
executable_folder := path_strip_filename(get_path_of_running_executable());
|
||||||
|
array_add(*candidates, executable_folder);
|
||||||
|
#if OS == .MACOS if ends_with(executable_folder, "/Contents/MacOS/") array_add(*candidates, tprint("%../Frameworks", executable_folder));
|
||||||
for folders array_add(*candidates, it);
|
for folders array_add(*candidates, it);
|
||||||
|
|
||||||
tried: [..] string;
|
tried: [..] string;
|
||||||
|
|||||||
10
src/App.jai
10
src/App.jai
@ -199,7 +199,7 @@ App :: struct {
|
|||||||
run_tests: bool;
|
run_tests: bool;
|
||||||
headless: bool; // -headless (or a no_crash_dialog build): no message boxes on failures, and run_tests exits when done
|
headless: bool; // -headless (or a no_crash_dialog build): no message boxes on failures, and run_tests exits when done
|
||||||
exit_code: s32; // the process's exit code when the main loop ends
|
exit_code: s32; // the process's exit code when the main loop ends
|
||||||
library_folder: string; // -library FOLDER: the .mcap folder the viewer shows, in place of VIEWER_FOLDER
|
library_folder: string; // the .mcap folder the viewer shows: -library FOLDER, else [library] folder, else LIBRARY_FOLDER_DEFAULT
|
||||||
scan_and_exit: bool; // -scan: scan the library with the chosen OCR model once it has loaded, then exit (0, or 1 if any capture failed)
|
scan_and_exit: bool; // -scan: scan the library with the chosen OCR model once it has loaded, then exit (0, or 1 if any capture failed)
|
||||||
stats_and_exit: bool; // -stats: log the Statistics window's numbers once the catalog has loaded, then exit
|
stats_and_exit: bool; // -stats: log the Statistics window's numbers once the catalog has loaded, then exit
|
||||||
catalog_threads: s64; // -catalog-threads N: workers loading the catalog at once, in place of all of the background group's (for measuring)
|
catalog_threads: s64; // -catalog-threads N: workers loading the catalog at once, in place of all of the background group's (for measuring)
|
||||||
@ -309,6 +309,14 @@ app_running_from_app_bundle :: () -> bool {
|
|||||||
return app_from_app_bundle;
|
return app_from_app_bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The folder the models' folders (ocr-models, label-models, segment-models) are in: the bundle's
|
||||||
|
// Contents/Resources on macOS, else beside the executable. Ends in a separator.
|
||||||
|
app_resource_directory :: () -> string {
|
||||||
|
directory := path_strip_filename(get_path_of_running_executable());
|
||||||
|
#if OS == .MACOS if app_running_from_app_bundle() return tprint("%../Resources/", directory);
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
#scope_file
|
#scope_file
|
||||||
app_install_directory_path: string;
|
app_install_directory_path: string;
|
||||||
app_from_app_bundle: bool;
|
app_from_app_bundle: bool;
|
||||||
|
|||||||
@ -53,7 +53,7 @@ labels_ui: struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
labels_models_folder :: () -> string {
|
labels_models_folder :: () -> string {
|
||||||
return tprint("%label-models", path_strip_filename(get_path_of_running_executable()));
|
return tprint("%label-models", app_resource_directory());
|
||||||
}
|
}
|
||||||
|
|
||||||
labels_list_path :: () -> string {
|
labels_list_path :: () -> string {
|
||||||
|
|||||||
@ -18,12 +18,7 @@ main :: () {
|
|||||||
app.headless = array_find(app.args, "-headless") || NO_CRASH_DIALOG;
|
app.headless = array_find(app.args, "-headless") || NO_CRASH_DIALOG;
|
||||||
runtime_support_headless = app.headless;
|
runtime_support_headless = app.headless;
|
||||||
app.allow_multiple_instances = array_find(app.args, "-allow-multiple-instances");
|
app.allow_multiple_instances = array_find(app.args, "-allow-multiple-instances");
|
||||||
for app.args if it == "-library" && it_index + 1 < app.args.count {
|
for app.args if it == "-library" && it_index + 1 < app.args.count app.library_folder = library_folder_normalize(app.args[it_index + 1]);
|
||||||
// With / between its folders and none at the end, as the paths found in it are made.
|
|
||||||
app.library_folder = copy_string(app.args[it_index + 1]);
|
|
||||||
path_overwrite_separators(app.library_folder, #char "/");
|
|
||||||
while app.library_folder.count > 3 && app.library_folder[app.library_folder.count - 1] == #char "/" app.library_folder.count -= 1;
|
|
||||||
}
|
|
||||||
app.scan_and_exit = array_find(app.args, "-scan");
|
app.scan_and_exit = array_find(app.args, "-scan");
|
||||||
app.segment_and_exit = array_find(app.args, "-segment");
|
app.segment_and_exit = array_find(app.args, "-segment");
|
||||||
for app.args if it == "-record" && it_index + 1 < app.args.count app.record_and_exit = max(string_to_float64(app.args[it_index + 1]), 1.0);
|
for app.args if it == "-record" && it_index + 1 < app.args.count app.record_and_exit = max(string_to_float64(app.args[it_index + 1]), 1.0);
|
||||||
@ -193,6 +188,8 @@ main :: () {
|
|||||||
|
|
||||||
settings_write_back := app_load_user_settings_file() && !app.view_mode; // the running instance owns the file
|
settings_write_back := app_load_user_settings_file() && !app.view_mode; // the running instance owns the file
|
||||||
logger_set_event_log_capacity(app_user_settings.event_log_capacity);
|
logger_set_event_log_capacity(app_user_settings.event_log_capacity);
|
||||||
|
library_folder_choose();
|
||||||
|
log("Library: %", app.library_folder);
|
||||||
|
|
||||||
// #Startup:CreateWindow
|
// #Startup:CreateWindow
|
||||||
#if USING_SDL {
|
#if USING_SDL {
|
||||||
|
|||||||
@ -106,7 +106,7 @@ Scan_Item :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ocr_models_folder :: () -> string {
|
ocr_models_folder :: () -> string {
|
||||||
return tprint("%ocr-models", path_strip_filename(get_path_of_running_executable()));
|
return tprint("%ocr-models", app_resource_directory());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- startup and shutdown
|
// ---------------------------------------------------------------- startup and shutdown
|
||||||
|
|||||||
@ -114,7 +114,7 @@ segment_colour_set :: (class: string, rgb: [3] u8, default := false, save := tru
|
|||||||
}
|
}
|
||||||
|
|
||||||
segment_models_folder :: () -> string {
|
segment_models_folder :: () -> string {
|
||||||
return tprint("%segment-models", path_strip_filename(get_path_of_running_executable()));
|
return tprint("%segment-models", app_resource_directory());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- startup and shutdown
|
// ---------------------------------------------------------------- startup and shutdown
|
||||||
|
|||||||
@ -16,10 +16,43 @@
|
|||||||
// appends the new ones. The library is watched (viewer_watch_frame), so files added or deleted
|
// appends the new ones. The library is watched (viewer_watch_frame), so files added or deleted
|
||||||
// outside the app come and go on their own; Refresh is still there for a watcher that missed some.
|
// outside the app come and go on their own; Refresh is still there for a watcher that missed some.
|
||||||
|
|
||||||
VIEWER_FOLDER :: "D:/McapIndex/mcap"; // hardcoded for now; -library FOLDER shows another
|
// The library when [library] folder in settings.config names none; -library FOLDER overrides both.
|
||||||
|
#if OS == .WINDOWS LIBRARY_FOLDER_DEFAULT :: "D:/McapIndex/mcap";
|
||||||
|
else LIBRARY_FOLDER_DEFAULT :: "~/Pictures/mcap";
|
||||||
|
|
||||||
|
// Set once at startup (library_folder_choose), so the library never changes under a running app.
|
||||||
viewer_folder :: () -> string {
|
viewer_folder :: () -> string {
|
||||||
return ifx app.library_folder then app.library_folder else VIEWER_FOLDER;
|
return app.library_folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -library, else the setting, else the default. Called once settings.config is read.
|
||||||
|
library_folder_choose :: () {
|
||||||
|
if app.library_folder return;
|
||||||
|
folder := ifx app_user_settings.library_folder.count then app_user_settings.library_folder else LIBRARY_FOLDER_DEFAULT;
|
||||||
|
app.library_folder = library_folder_normalize(folder);
|
||||||
|
|
||||||
|
// The default is made on first use, so the catalog cache and imports have somewhere to go. Any
|
||||||
|
// other folder is left to the user: a mistyped one is not made.
|
||||||
|
default := library_folder_normalize(LIBRARY_FOLDER_DEFAULT);
|
||||||
|
defer free(default,, context.default_allocator);
|
||||||
|
if app.library_folder == default && !file_exists(app.library_folder) {
|
||||||
|
if make_directory_if_it_does_not_exist(app.library_folder, recursive = true) log("[library] Made the library folder %.", app.library_folder);
|
||||||
|
else log_error("[library] Could not make the library folder %.", app.library_folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With / between its folders and none at the end, as the paths found in it are made, and a leading
|
||||||
|
// ~/ for the home folder. Heap-allocated.
|
||||||
|
library_folder_normalize :: (folder: string) -> string {
|
||||||
|
expanded := folder;
|
||||||
|
#if OS != .WINDOWS if begins_with(folder, "~/") || folder == "~" {
|
||||||
|
home := to_string(POSIX.getenv("HOME"));
|
||||||
|
if home.count expanded = tprint("%1%2", trim_right(home, "/"), advance(folder, 1));
|
||||||
|
}
|
||||||
|
result := copy_string(expanded,, context.default_allocator);
|
||||||
|
path_overwrite_separators(result, #char "/");
|
||||||
|
while result.count > 3 && result[result.count - 1] == #char "/" result.count -= 1;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
VIEWER_WORKER_COUNT :: 4;
|
VIEWER_WORKER_COUNT :: 4;
|
||||||
VIEWER_MAX_TEXTURES :: 1200; // previews on the GPU at once, about 200 KB each
|
VIEWER_MAX_TEXTURES :: 1200; // previews on the GPU at once, about 200 KB each
|
||||||
|
|||||||
@ -296,6 +296,9 @@ App_User_Settings :: struct {
|
|||||||
// [export] folder, one line each: the folders Export offers for a capture's original, the first
|
// [export] folder, one line each: the folders Export offers for a capture's original, the first
|
||||||
// the default. None means the Downloads folder. Heap-allocated.
|
// the default. None means the Downloads folder. Heap-allocated.
|
||||||
export_folders: [..] string;
|
export_folders: [..] string;
|
||||||
|
// [library] folder: the library, set by hand and read at startup; "" means LIBRARY_FOLDER_DEFAULT.
|
||||||
|
// Heap-allocated.
|
||||||
|
library_folder: string;
|
||||||
|
|
||||||
// Of the monitor the window first opens on.
|
// Of the monitor the window first opens on.
|
||||||
window_width_fraction: float = 0.80;
|
window_width_fraction: float = 0.80;
|
||||||
@ -328,6 +331,7 @@ settings_free :: (s: *App_User_Settings) {
|
|||||||
free(s.ocr_model,, context.default_allocator);
|
free(s.ocr_model,, context.default_allocator);
|
||||||
for s.export_folders free(it,, context.default_allocator);
|
for s.export_folders free(it,, context.default_allocator);
|
||||||
array_free(s.export_folders);
|
array_free(s.export_folders);
|
||||||
|
free(s.library_folder,, context.default_allocator);
|
||||||
free(s.video_capture,, context.default_allocator);
|
free(s.video_capture,, context.default_allocator);
|
||||||
array_free(s.hotkey_bindings);
|
array_free(s.hotkey_bindings);
|
||||||
array_free(s.global_hotkey_bindings);
|
array_free(s.global_hotkey_bindings);
|
||||||
@ -387,11 +391,11 @@ app_load_user_settings_file :: () -> write_back: bool {
|
|||||||
if hotkeys_missing.count || global_missing.count return true;
|
if hotkeys_missing.count || global_missing.count return true;
|
||||||
|
|
||||||
#if OS == .LINUX seen.global_hotkeys = true; // not written there: Linux has no global hotkeys yet
|
#if OS == .LINUX seen.global_hotkeys = true; // not written there: Linux has no global hotkeys yet
|
||||||
return !(seen.theme && seen.width && seen.height && seen.font_size && seen.font_scale && seen.event_log && seen.hotkeys && seen.global_hotkeys && seen.ocr_model && seen.export_folder && seen.video_fps && seen.video_cursor && seen.video_capture && seen.video_system_audio && seen.video_microphone);
|
return !(seen.theme && seen.width && seen.height && seen.font_size && seen.font_scale && seen.event_log && seen.hotkeys && seen.global_hotkeys && seen.ocr_model && seen.export_folder && seen.library_folder && seen.video_fps && seen.video_cursor && seen.video_capture && seen.video_system_audio && seen.video_microphone);
|
||||||
}
|
}
|
||||||
|
|
||||||
Settings_Seen :: struct {
|
Settings_Seen :: struct {
|
||||||
theme, width, height, font_size, font_scale, event_log, hotkeys, global_hotkeys, ocr_model, export_folder: bool;
|
theme, width, height, font_size, font_scale, event_log, hotkeys, global_hotkeys, ocr_model, export_folder, library_folder: bool;
|
||||||
video_fps, video_cursor, video_capture, video_system_audio, video_microphone: bool;
|
video_fps, video_cursor, video_capture, video_system_audio, video_microphone: bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -522,6 +526,10 @@ settings_parse :: (path: string, file_data: string, out: *App_User_Settings) ->
|
|||||||
folder := settings_unquote(value); // an empty line says the list is empty
|
folder := settings_unquote(value); // an empty line says the list is empty
|
||||||
if folder.count array_add(*out.export_folders, copy_string(folder,, context.default_allocator));
|
if folder.count array_add(*out.export_folders, copy_string(folder,, context.default_allocator));
|
||||||
seen.export_folder = true;
|
seen.export_folder = true;
|
||||||
|
} else if section == "[library]" && key == "folder" {
|
||||||
|
free(out.library_folder,, context.default_allocator);
|
||||||
|
out.library_folder = copy_string(settings_unquote(value),, context.default_allocator);
|
||||||
|
seen.library_folder = true;
|
||||||
} else if section == "[ui]" && key == "theme" {
|
} else if section == "[ui]" && key == "theme" {
|
||||||
free(out.theme,, context.default_allocator);
|
free(out.theme,, context.default_allocator);
|
||||||
out.theme = copy_string(settings_unquote(value),, context.default_allocator);
|
out.theme = copy_string(settings_unquote(value),, context.default_allocator);
|
||||||
@ -686,6 +694,10 @@ app_save_user_settings_file :: () -> bool {
|
|||||||
if seen.width app_user_settings.window_width_fraction = on_disk.window_width_fraction;
|
if seen.width app_user_settings.window_width_fraction = on_disk.window_width_fraction;
|
||||||
if seen.height app_user_settings.window_height_fraction = on_disk.window_height_fraction;
|
if seen.height app_user_settings.window_height_fraction = on_disk.window_height_fraction;
|
||||||
if seen.event_log app_user_settings.event_log_capacity = on_disk.event_log_capacity;
|
if seen.event_log app_user_settings.event_log_capacity = on_disk.event_log_capacity;
|
||||||
|
if seen.library_folder {
|
||||||
|
free(app_user_settings.library_folder,, context.default_allocator);
|
||||||
|
app_user_settings.library_folder = copy_string(on_disk.library_folder,, context.default_allocator);
|
||||||
|
}
|
||||||
if seen.video_fps app_user_settings.video_fps = on_disk.video_fps;
|
if seen.video_fps app_user_settings.video_fps = on_disk.video_fps;
|
||||||
if seen.video_cursor app_user_settings.video_cursor = on_disk.video_cursor;
|
if seen.video_cursor app_user_settings.video_cursor = on_disk.video_cursor;
|
||||||
// video_system_audio and video_microphone are the app's: the recording toolbar's buttons set them.
|
// video_system_audio and video_microphone are the app's: the recording toolbar's buttons set them.
|
||||||
@ -719,6 +731,10 @@ app_save_user_settings_file :: () -> bool {
|
|||||||
for export_folders print_to_builder(*builder, "folder: %\n", settings_quote_if_needed(it));
|
for export_folders print_to_builder(*builder, "folder: %\n", settings_quote_if_needed(it));
|
||||||
if !export_folders append(*builder, "folder:\n");
|
if !export_folders append(*builder, "folder:\n");
|
||||||
append(*builder, "\n");
|
append(*builder, "\n");
|
||||||
|
append(*builder, "[library]\n");
|
||||||
|
append(*builder, "# The folder of .mcap files mcap shows and imports into; ~/ is the home folder. Read when mcap\n");
|
||||||
|
append(*builder, "# starts; -library FOLDER on the command line overrides it.\n");
|
||||||
|
print_to_builder(*builder, "folder: %\n\n", settings_quote_if_needed(ifx app_user_settings.library_folder.count then app_user_settings.library_folder else LIBRARY_FOLDER_DEFAULT));
|
||||||
append(*builder, "[video]\n");
|
append(*builder, "[video]\n");
|
||||||
append(*builder, "# Screen recording (Ctrl+PrintScreen; Windows). fps: frames a second at most, written only when the\n");
|
append(*builder, "# Screen recording (Ctrl+PrintScreen; Windows). fps: frames a second at most, written only when the\n");
|
||||||
append(*builder, "# screen changes. cursor: true or false. capture: auto (Desktop Duplication, else\n");
|
append(*builder, "# screen changes. cursor: true or false. capture: auto (Desktop Duplication, else\n");
|
||||||
@ -853,6 +869,10 @@ app_reload_user_settings_if_changed :: () {
|
|||||||
|
|
||||||
if fresh.event_log_capacity != old.event_log_capacity logger_set_event_log_capacity(fresh.event_log_capacity);
|
if fresh.event_log_capacity != old.event_log_capacity logger_set_event_log_capacity(fresh.event_log_capacity);
|
||||||
if fresh.ocr_model != old.ocr_model ocr_ui_settings_changed(); // app/ocr_ui.jai
|
if fresh.ocr_model != old.ocr_model ocr_ui_settings_changed(); // app/ocr_ui.jai
|
||||||
|
if fresh.library_folder != old.library_folder {
|
||||||
|
log("[settings] The library folder is now %; mcap shows it from its next start.", ifx fresh.library_folder.count then fresh.library_folder else LIBRARY_FOLDER_DEFAULT);
|
||||||
|
toast_show(.Success, "The new library folder is shown when mcap starts again");
|
||||||
|
}
|
||||||
|
|
||||||
bindings: [] Hotkey_Binding = hotkey_defaults();
|
bindings: [] Hotkey_Binding = hotkey_defaults();
|
||||||
if seen.hotkeys bindings = app_user_settings.hotkey_bindings;
|
if seen.hotkeys bindings = app_user_settings.hotkey_bindings;
|
||||||
@ -881,7 +901,8 @@ settings_watch: struct {
|
|||||||
|
|
||||||
app_settings_watch_start :: () {
|
app_settings_watch_start :: () {
|
||||||
dir := app_program_data_dir();
|
dir := app_program_data_dir();
|
||||||
if !Watcher.init(*settings_watch.watcher, settings_watch_callback, watch_recursively = false) {
|
// macOS's FSEvents always watches recursively, and the module logs an error when asked not to.
|
||||||
|
if !Watcher.init(*settings_watch.watcher, settings_watch_callback, watch_recursively = OS == .MACOS) {
|
||||||
log_error("[settings] Could not watch %. Changes to % apply at the next start.", dir, APP_USER_SETTINGS_FILE_NAME);
|
log_error("[settings] Could not watch %. Changes to % apply at the next start.", dir, APP_USER_SETTINGS_FILE_NAME);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user