mcap/src/App.jai
Musa Mahmood be9ad707c4 [0.1.17] mcap.exe FILE opens a file, beside a running mcap in view mode; a Command Line window; thumbnails no longer stuck on "Cannot read"
mcap.exe FILE, with FILE first (as Explorer's Open with passes it), opens an .mcap, image, video or
.snagx (app/open_file.jai). With no mcap running, the app starts as usual: an .mcap in the library
opens in the Capture window, anything else in the Opened File window, which says where the library
has it already (by its hash in .import-index) or imports it into the library folder typed or chosen.
With mcap running, a second window opens in view mode: the file alone, with Import, and Open in mcap
/ Show in mcap, which hand it to the running copy (a new Open_File message) and close the view. The
library is not loaded or watched, no model loads, and it has no tray icon, no global hotkeys, and
writes no settings or layout.

Import takes a single file as its source too (and -import FILE DEST); an .mcap is copied in as it is,
while a folder's import still skips them. -open-import DEST imports the opened file as its Import
button does, then exits.

-h and --help open a Command Line window listing the options, also toggled from the Debug Panel; with
mcap running, that one comes forward with it (a Show_Help message).

Thumbnails: the "First preview shown" log line, put between creating a preview's texture and its
else, had every preview after the first marked failed. Once its texture was released (scrolled far
away, or the switch from WARP to the GPU at startup) it was never decoded again and showed "Cannot
read".
2026-10-14 08:00:02 +01:00

374 lines
13 KiB
Plaintext

USING_SDL :: OS == .LINUX || OS == .MACOS;
USING_WINDOWS :: OS == .WINDOWS;
#if !USING_SDL && !USING_WINDOWS
#assert false "Invalid configuration";
// The arena and fixed_arena suites live in modules/Basic/tests and arrive with LOAD_TESTS
// on the Basic import below.
#if COMPILE_TESTS {
#load "test/testing.jai";
#load "test/logger_tests.jai";
#load "test/catalog_tests.jai";
#load "test/ocr_tests.jai";
#load "test/labels_tests.jai";
#load "test/import_tests.jai";
}
#load "app/main.jai"; // entry point
#load "app/app_ui.jai";
#load "app/viewer.jai";
#load "app/ocr_ui.jai";
#load "app/catalog.jai";
#load "app/stats_ui.jai";
#load "app/background.jai";
#load "app/labels_ui.jai";
#load "app/import.jai";
#load "app/open_file.jai";
#load "app/segment_ui.jai";
#load "app/library_folders.jai";
#load "app/exit_shutdown.jai";
#load "app/screen_capture.jai";
#load "app/screen_record.jai";
#if OS == .WINDOWS #load "app/screen_capture_win32.jai";
#if OS == .WINDOWS #load "app/screen_record_win32.jai";
#load "base/circle_buffer.jai";
#load "base/debug.jai";
#load "base/fonts.jai";
#load "base/log_queue.jai";
#load "base/logger.jai";
#load "base/prune_files.jai";
#load "ui/imgui.jai";
#load "ui/imgui_keyboard.jai";
#load "ui/imgui_theme.jai";
#load "ui/imgui_theme_file.jai";
#load "ui/imgui_theme_registry.jai";
#load "ui/imgui_theme_editor.jai";
#load "ui/imgui_toast.jai";
#load "ui/imgui_exit_overlay.jai";
#import "Atomics";
#import "Basic"()(MEMORY_DEBUGGER=false,ARENA_DEBUG=DEBUG,LOAD_TESTS=COMPILE_TESTS);
#import "Clipboard";
#import "String";
#import "Sort";
#import "Unicode";
#import "File";
#import "Random"; // the noise viewer_overwrite_and_delete writes
#import "Float16"; // label vectors are stored as 16-bit floats
#import "File_Utilities";
#import "Math";
#import "System";
#import "Thread";
#import "Hash_Table";
#import "Hash"; // fnv1a_hash, for the label list's hash
Default_Allocator :: #import "Default_Allocator"; // rpmalloc (modules/Default_Allocator), for thread_idle at the workers' waits
Shared_Channel :: #import "Shared_Memory_Channel";
#import "stb_image";
#import "stb_image_resize";
#import "Capture_File"; // the .mcap format, copied from D:\McapIndex\SnagitConverter
#import "Image_Decode"; // any image file to pixels: stb_image, and WIC on Windows
#import "Snagx"; // .snagx captures to .mcap, as SnagitConverter makes them
#import,dir "../modules/jaison/unicode_utils"; // is_valid_utf8
#import "OCR";
#import "Labels";
#import "Segment";
#import "Capture_Text";
#import "Base64";
#import "onnxruntime";
#if OS == .WINDOWS #import "Screen_Record"; // screen recording: Desktop Duplication or Windows.Graphics.Capture, to NVENC
// Basic's arenas import Secure_Memory at its defaults; this import adds its test suite.
using Secure_Memory :: #import "Secure_Memory"()(LOAD_TESTS=COMPILE_TESTS);
Graphics_Backend :: enum s32 {
Unknown :: 0;
DX11;
Vulkan;
OpenGL;
Metal;
DX12;
}
graphics_backend_from_string :: (name: string) -> Graphics_Backend {
if name == {
case "vulkan"; return .Vulkan;
case "opengl"; return .OpenGL;
case "metal"; return .Metal;
case "direct3d11"; return .DX11;
case "direct3d12"; return .DX12;
}
assert(false, "Unknown Graphics_Backend!");
return .Unknown;
}
Memory_Usage :: struct {
working_set: s64;
virtual_memory: s64;
peak_working_set: s64;
peak_virtual_memory: s64;
}
perf: Perf;
Perf :: struct {
backend: Graphics_Backend;
renderer_name: string;
adapter_name: string; // e.g. "NVIDIA GeForce RTX 4070"; empty when the adapter could not be queried
renderer_is_software: bool;
time_to_first_frame_ms: float64;
// See #WarpFirstStartup in graphics/DX11.jai. hw_device_ready_ms is measured from app.startup_time.
started_on_warp: bool;
startup_device_ms: float64; // D3D11CreateDevice for the startup device
startup_swap_chain_ms: float64; // its swap chain and render target
hw_device_create_ms: float64;
hw_device_ready_ms: float64;
hw_swap_ms: float64;
memory_usage: Memory_Usage;
frame_times: Circle_Buffer(Frame_Timing);
}
Frame_Timing :: struct {
work: float32; // seconds from the start of app_frame to just before Present (doesn't include vsync or swapchain waits)
delta: float32; // io.DeltaTime: wall time since the previous frame, vsync wait included
}
PERF_FRAME_TIME_HISTORY :: 1024;
load_hardware_information :: () {
{ // Get CPU information:
using cpu_info;
physical_cpu_count = get_number_of_processors(.ALL_PHYSICAL);
logical_cpu_count = get_number_of_processors(.ALL_LOGICAL);
performance_cpu_count = get_number_of_processors(.PERFORMANCE_PHYSICAL);
}
#if OS == .WINDOWS windows_get_cpu_information();
}
load_platform_information :: () {
platform_load_platform_information();
}
cpu_info: CPU_Info;
CPU_Info :: struct {
logical_cpu_count: s32;
physical_cpu_count: s32;
performance_cpu_count: s32;
}
CPUID_Type :: enum s32 {
All :: 0; // All Logical CPUs
Performance :: 1;
Efficiency :: 2;
// Top_Performance :: 64; // Typically two CPUs are marked with this value
}
platform: Platform; // Windows, MacOS, Linux
app: App;
App :: struct {
frame_count: s64;
startup_time: Apollo_Time;
frame_time: Apollo_Time;
win_scale: float32;
minimized_to_tray: bool;
was_minimized_to_tray: bool; // as of the last frame; main.jai drops stale input when it changes
vsync_on: bool = true;
sleep_on_fast_frame: bool = false;
should_exit: bool = false;
show_debug_panel: bool = DEBUG;
show_logger_panel: bool = DEBUG;
show_frame_time_graph: bool = DEBUG;
show_style_editor: bool;
show_help_panel: bool; // the Command Line window (base/debug.jai): -h, --help, or the Debug Panel
help_panel_focus: bool; // bring it forward next frame
minimize_to_tray_this_frame: bool;
restore_window_this_frame: bool;
capture_region_this_frame: bool; // the capture_region global hotkey fired: app/screen_capture.jai
record_region_this_frame: bool; // the record_region global hotkey fired: app/screen_record.jai
is_master: bool; // Are we the first instance of this app launched?
allow_multiple_instances: bool; // Skips the single-instance check, so a dev build can run beside an installed one.
should_exit_early: bool;
run_tests: bool;
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
library_folder: string; // -library FOLDER: the .mcap folder the viewer shows, in place of VIEWER_FOLDER
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
catalog_threads: s64; // -catalog-threads N: workers loading the catalog at once, in place of all of the background group's (for measuring)
cold_start: bool;
no_catalog_cache: bool; // -no-catalog-cache: read every capture from its file, as without the catalog cache (for measuring)
launch_time: Apollo_Time; // monotonic, when main() started: the [startup] log lines count from it // -cold: load the library as on a cold start, its files dropped from the system's file cache first
start_search: string; // -search QUERY: the search box's text at startup
erase_and_exit: string; // -erase ocr|labels|segments|all: erase that from the whole library once it has loaded, then exit
record_and_exit: float64; // -record SECONDS: record the screen under the cursor that long, put it in the library, then exit (0, or 1 if it failed)
#if OS == .WINDOWS record_region: RECT; // -record-region X Y W H: -record records that, on the virtual desktop, not the whole screen
segment_and_exit: bool; // -segment: segment the library folders set to segment once the catalog has loaded, then exit (0, or 1 if any image failed)
import_source, import_dest: string; // -import SOURCE DEST: import SOURCE into the library folder DEST (app/import.jai), then exit
open_file: string; // mcap.exe FILE: the file to show (app/open_file.jai); absolute, with / between its folders
view_mode: bool; // FILE was given while another instance runs: only it is shown, with Import (app/open_file.jai)
open_import_dest: string; // -open-import DEST: FILE imported into the library folder DEST as its Import button does, then exit (0, or 1 if it failed)
last_window_restore_time: Apollo_Time;
imgui_context: *ImGui.ImGuiContext;
write_channel: Shared_Channel.Write_Channel;
read_channel: Shared_Channel.Read_Channel;
read_channel_fatal_errors: s32; // the first resets the queue, the second stops polling
large_pages_allowed: bool;
#if USING_WINDOWS {
is_admin: bool; // running with elevated privileges?
ui_global_clear_color: [4]float : .[0.45, 0.55, 0.60, 1.00];
} else #if USING_SDL {
ui_global_clear_color: SDL_FColor = .{ r=0.45, g=0.55, b=0.60, a=1.0 };
}
program_data_dir: string; // "<os program data>/<APP_FOLDER_NAME>"; see app_program_data_dir()
machine_name: string;
username: string;
args: []string;
}
#if USING_SDL {
// This uses SDL3 and SDL_GPU
ImGui :: #import "imgui-sdl";
#import "sdl3";
#import "freetype";
// Load the backend(s) you want. This *must* be #load because they might rely on other libraries.
// In this case, sdl3_gpu3 relies on types from "sdl3", so by importing sdl3 and loading imgui, imgui
// gets access to the types it needs.
#load "backends/sdl3_gpu3.jai";
#load "freetype/freetype.jai";
#load "base/platform/sdl.jai";
#load "base/platform/platform_posix.jai";
#if OS == .LINUX {
#load "base/platform/platform_linux.jai";
POSIX :: #import "POSIX";
}
else #if OS == .MACOS {
#load "base/platform/platform_macos.jai";
#load "base/platform/macos_bindings.jai";
#load "base/platform/macos_global_hotkeys.jai";
POSIX :: #import "POSIX";
}
APP_TRAY_ICON_PATH :: "images/tray_icon.png";
}
#if USING_WINDOWS {
// This uses Win32 API directly and DX11 for graphics
ImGui :: #import "imgui-dx11-win32"()(DEBUG = DEBUG);
#import "d3d11";
#import "d3d_compiler";
#import "dxgi";
#import "Windows";
#import "Windows_Utf8";
#load "base/platform/platform_windows.jai";
#load "base/platform/win32_bindings.jai";
#load "base/platform/win32_gdi_bindings.jai";
#load "graphics/DX11.jai";
APP_TRAY_ICON_PATH :: "images/tray_icon.ico";
}
// The folder the app's data, logs and images live in: the executable's, or on macOS the one
// holding the .app when run from the bundle, so replacing the bundle leaves the data alone. Ends in
// a separator, as path_strip_filename does. Set by the first call, which the logger makes at startup.
app_install_directory :: () -> string {
if !app_install_directory_path {
directory := path_strip_filename(get_path_of_running_executable(,, temp));
#if OS == .MACOS {
BUNDLE_EXECUTABLE_FOLDER :: "/Contents/MacOS/";
if ends_with(directory, tprint(".app%", BUNDLE_EXECUTABLE_FOLDER)) {
directory = path_strip_filename(slice(directory, 0, directory.count - BUNDLE_EXECUTABLE_FOLDER.count));
app_from_app_bundle = true;
}
}
app_install_directory_path = copy_string(directory,, context.default_allocator);
}
return app_install_directory_path;
}
app_running_from_app_bundle :: () -> bool {
app_install_directory();
return app_from_app_bundle;
}
#scope_file
app_install_directory_path: string;
app_from_app_bundle: bool;
#scope_export
get_app_tray_icon_path :: () -> string {
push_allocator(temp);
icon_path := tprint("%1%2", app_install_directory(), APP_TRAY_ICON_PATH);
log("icon_path: %", icon_path);
return icon_path;
}
APP_LOG_DIRECTORY :: #run -> string {
return tprint("logs/%", VERSION);
}
APP_USER_SETTINGS_FILE_NAME :: "settings.config";
APP_USER_SETTINGS_VERSION :: 1;
// A file at any version from this one through APP_USER_SETTINGS_VERSION is readable. Raise
// APP_USER_SETTINGS_VERSION when an action is added to [hotkeys] or [global_hotkeys]; see
// hotkeys_missing_lines.
APP_USER_SETTINGS_OLDEST_READABLE_VERSION :: 1;
APP_SHARED_MEMORY_CHANNEL_HEADER_SIZE :: 2; // 1 byte for message-or-done, 1 byte for the checksum.
App_Shared_Memory_Tag :: enum u8 {
Done :: 0;
Bring_To_Front :: 1;
Open_File :: 2; // the path follows the header: show it (open_file_request)
Show_Help :: 3; // mcap --help while this one runs: come forward with the Command Line window
}
APP_SHARED_MEMORY_CHANNEL_BYTES :: 4096; // only one page?
APP_WINDOW_TITLE :: #run -> string {
append_debug := ifx DEBUG then " (DEBUG)\0" else " (RELEASE)\0";
git_info := ifx GIT_BRANCH != "master" && GIT_BRANCH != "main" then
tprint(" %-%", GIT_BRANCH, GIT_HASH) else tprint(" %", GIT_HASH);
return tprint("%1 %2%3%4", APP_NAME, VERSION,
git_info, append_debug);
}
// The window's title, ending in a 0 for the OS: in view mode, the file's name first.
app_window_title :: () -> string {
if !app.view_mode return APP_WINDOW_TITLE;
return tprint("% - % (view)\0", path_filename(app.open_file), APP_NAME);
}
APP_TRAY_TOOLTIP_TEXT :: #run -> string {
return ifx DEBUG
tprint("% % (DEBUG)\0", APP_NAME, VERSION)
else
tprint("% %\0", APP_NAME, VERSION);
}
#if OS == .LINUX {
clipboard_set_text :: (text: string) {
ImGui.SetClipboardText(temp_c_string(text));
}
} else {
clipboard_set_text :: (text: string) {
os_clipboard_set_text(text);
}
}