Compare commits

..

2 Commits

Author SHA1 Message Date
2bd77e8121 [0.1.16] README: the server OCR model's bimodal speed on the GPU, as a known issue
The README's "about 0.4 s" for the server model was measured on 2026-10-07 (e024b7f), and the
batching it ran with has not changed since. Measured now, the same build on the same 20 captures
runs at about 0.27 s a capture with 2.4 GB at peak, or about 1.5 s with 6.6 GB, with either
allocator; longer runs fare worse. Until that is fixed, the mobile model is the one to scan a large
library with.
2026-10-13 08:30:50 +01:00
03f23f7122 [0.1.16] Ctrl-C copies the selected capture's image, Ctrl-Shift-C its full path
The selected capture is the one outlined in the grid and shown in the Capture window. Its image is
copied as the Capture window shows it, decoded on a thread of its own and put on the clipboard as
screen captures are (Windows only so far); its path is copied as the system writes paths. Both are
actions in [hotkeys] (copy_image, copy_path), which do not act while typing in the search box.
Settings files at the current version get their default bindings at the next start; the comment
saying to raise APP_USER_SETTINGS_VERSION for a new action had it backwards.
2026-10-13 07:36:52 +01:00
7 changed files with 120 additions and 5 deletions

View File

@ -58,6 +58,11 @@ see [Screen capture](#screen-capture).
move the file to the Recycle Bin or to delete it permanently, overwriting it with random bytes
first; "Don't ask again" keeps the choice until the app restarts. Only the `.mcap` goes: the
`.snagx` it was converted from is untouched.
- **Copy:** click a thumbnail to select it (it is outlined, and shown in the Capture window), then
**Ctrl-C** copies its image, as the Capture window shows it (annotations and all; a video's
frame), and **Ctrl-Shift-C** its full path. Neither acts while you type in the search box. They
are `copy_image` and `copy_path` in `[hotkeys]` in `settings.config`. Copying images is Windows
only so far.
- **Files added or deleted outside the app** (in Explorer, by another program) appear in the grid
or leave it on their own, folders added, renamed or deleted too: the library is watched.
**Refresh** looks at the whole folder again, should the watcher have missed something.
@ -93,7 +98,18 @@ grid (in its order, so through a search's matches).
### The OCR window
- **Model:** PP-OCRv5 mobile (light, about 85 ms a capture on the GPU) or server (more accurate,
about 0.4 s). The choice is kept in the settings file (`model` under `[ocr]`).
about 0.3 s when it runs well; see below). The choice is kept in the settings file (`model` under
`[ocr]`).
- **Known issue, the server model on the GPU:** its speed is bimodal. The same program on the same
20 captures (RTX 5080 laptop, 16 GB) runs either at about 0.27 s a capture with a peak of about
2.4 GB of process memory, or at about 1.5 s with about 6.6 GB, with no change between runs and
15 GB of the GPU's memory free beforehand. Longer runs fare worse: 50 captures peaked at 16 GB, and
100 averaged about 4.6 s a capture. The extra memory looks like GPU allocations spilling into shared
system memory, which Windows then pages: what the GPU memory defaults (onnxruntime's
kNextPowerOfTwo arena, no cuDNN max workspace, 4 workers) were meant to stop, and which they
only partly did. It is the same with Jai's allocator and with rpmalloc. Until it is fixed, scan a
large library with the mobile model. Measured on 2026-10-13 with
`ocr-bench -models ppocr-v5-server -gpu-only -limit 20`.
- **Running on:** the GPU or the CPU, and if the CPU, what to install for the GPU.
- **Scan the library** reads every capture the model has not read yet and, with **Labels too**,
labels every capture not yet labelled with the current list. While a search is active the button

View File

@ -18,6 +18,10 @@ Ctrl-Equal increase_font_size
Ctrl-Alt-Space reset_window
Ctrl-Shift-Alt-R reset_layout
# Library: the selected capture
Ctrl-C copy_image
Ctrl-Shift-C copy_path
# Debug
Ctrl-Shift-D toggle_debug_panel
Ctrl-Shift-L toggle_logger_panel

View File

@ -18,6 +18,10 @@ Cmd-Equal increase_font_size
Cmd-Opt-Shift-Space reset_window
Cmd-Shift-Opt-R reset_layout
# Library: the selected capture
Cmd-C copy_image
Cmd-Shift-C copy_path
# Debug
Cmd-Shift-D toggle_debug_panel
Cmd-Shift-L toggle_logger_panel

View File

@ -47,7 +47,7 @@ APP_NAME :: "mcap";
APP_FOLDER_NAME :: "mcap";
GIT_BRANCH :: "check";
GIT_HASH :: "-";
VERSION :: "0.1.15";
VERSION :: "0.1.16";
BUILD_DATE :: "-";
DEBUG :: true;
COMPILE_TESTS :: true;

View File

@ -8,7 +8,7 @@ APP_NAME :: "mcap";
APP_FOLDER_NAME :: "mcap";
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
VERSION :: "0.1.15";
VERSION :: "0.1.16";
// 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.
build_directory := "bin";

View File

@ -156,6 +156,14 @@ viewer: struct {
open_name: string;
open_result: string; // the file written, or ""; heap, the open thread's until it ends
open_error: string;
// The selected capture's image, decoded off the main thread for the clipboard (viewer_copy_image).
copy_thread: Thread;
copy_started: bool;
copy_busy: bool; // set by the main thread, cleared by the copy thread when it ends
copy_source: string; // the .mcap; heap, main thread's until the thread starts
copy_name: string;
copy_done: Decoded; // the copy thread's until it ends
}
// ---------------------------------------------------------------- startup and shutdown
@ -229,6 +237,7 @@ viewer_shutdown :: () {
for 1..viewer.workers_started signal(*viewer.wake);
for 0..viewer.workers_started - 1 thread_is_done(*viewer.workers[it], -1);
if viewer.open_started thread_is_done(*viewer.open_thread, -1);
if viewer.copy_started thread_is_done(*viewer.copy_thread, -1);
background_shutdown();
catalog_cache_save_at_shutdown(); // the workers have stopped: nothing changes the entries now
viewer_watch_stop();
@ -451,6 +460,7 @@ viewer_frame_gpu_work :: () {
viewer_apply_catalog_updates();
catalog_cache_frame(); // saves the catalog cache once it is complete and has changed (app/catalog.jai)
viewer_open_frame();
viewer_copy_frame();
segment_overlay_frame(); // the Capture window's segments: what arrived, and its texture (app/segment_ui.jai)
background_frame(); // takes back search results, and starts a search when visible_dirty (app/background.jai)
}
@ -1215,6 +1225,77 @@ viewer_open_frame :: () {
else toast_show(.Error, "Could not write % out: %", viewer.open_name, viewer.open_error);
}
// ---------------------------------------------------------------- copying the selected capture
// The selected capture: the one outlined in the grid and shown in the Capture window. -1 when none.
viewer_selected :: () -> s32 {
index := viewer.full_index;
if index < 0 || index >= viewer.captures.count || viewer.captures[index].removed return -1;
return index;
}
// Ctrl-Shift-C (copy_path in [hotkeys]): the selected capture's path, as the system writes paths.
viewer_copy_path :: () {
index := viewer_selected();
if index < 0 { toast_show(.Error, "Select a capture to copy its path"); return; }
capture := *viewer.captures[index];
path := capture.path;
#if OS == .WINDOWS path = replace(path, "/", "\\",, temp);
clipboard_set_text(path);
toast_show(.Success, "Copied the path of %", capture.name);
}
// Ctrl-C (copy_image in [hotkeys]): the selected capture's image, as the Capture window shows it
// (annotations and all; a video's frame), decoded on a thread of its own and put on the clipboard
// by viewer_copy_frame.
viewer_copy_image :: () {
index := viewer_selected();
if index < 0 { toast_show(.Error, "Select a capture to copy its image"); return; }
capture := *viewer.captures[index];
#if OS != .WINDOWS {
toast_show(.Error, "Copying images is not available on this system yet");
return;
}
if viewer.copy_started { toast_show(.Error, "Still copying %", viewer.copy_name); return; }
push_allocator(context.default_allocator);
free(viewer.copy_source); free(viewer.copy_name);
viewer.copy_source = copy_string(capture.path);
viewer.copy_name = copy_string(capture.name);
viewer.copy_done = .{};
viewer.copy_busy = true;
// Reading an image takes a few MB of temporary storage, as on the viewer's workers.
viewer.copy_started = thread_init(*viewer.copy_thread, viewer_copy_proc, temporary_storage_size = 1024 * 1024, name = "copy image");
if !viewer.copy_started { viewer.copy_busy = false; toast_show(.Error, "Could not copy %", capture.name); return; }
thread_start(*viewer.copy_thread);
}
viewer_copy_proc :: (thread: *Thread) -> s64 {
context.allocator = context.default_allocator;
defer atomic_swap(*viewer.copy_busy, false);
viewer.copy_done = viewer_decode_full(viewer.copy_source);
return 0;
}
// Each frame: puts what viewer_copy_proc decoded on the clipboard, once it is done.
viewer_copy_frame :: () {
if !viewer.copy_started || viewer.copy_busy return;
thread_is_done(*viewer.copy_thread, -1);
thread_deinit(*viewer.copy_thread);
viewer.copy_started = false;
done := *viewer.copy_done;
defer viewer_free_pixels(done);
if done.failed { toast_show(.Error, "Could not read the image of %; see the Event Log", viewer.copy_name); return; }
pixels := []u8.{cast(s64) done.width * done.height * 4, done.pixels};
if !platform_copy_image_to_clipboard(pixels, done.width, done.height) {
log_error("[viewer] Could not put the image of % on the clipboard.", viewer.copy_source);
toast_show(.Error, "Could not put % on the clipboard", viewer.copy_name);
return;
}
scaled := ifx done.width != done.source_width || done.height != done.source_height then tprint(", scaled down from % x %", done.source_width, done.source_height) else "";
log("[viewer] Copied the image of % (% x %%).", viewer.copy_source, done.width, done.height, scaled);
toast_show(.Success, "Copied % (% x %%)", viewer.copy_name, done.width, done.height, scaled);
}
viewer_grid :: () { using ImGui;
style := GetStyle();
cell := viewer.thumb_size;

View File

@ -148,8 +148,9 @@ to_string_all_bindings :: (hotkey: Hotkey) -> string {
// The bindings are read from settings.config, or from DEFAULT_HOTKEYS_DATA when the file has
// no [hotkeys] section; see hotkeys_install. once and ignore_when_text_field_active belong to the
// action and are set here. To add an action: declare its Hotkey here, add it to
// hotkey_actions_build and app_check_key_combinations, bind it in both assets/data/default_hotkeys
// files, and raise APP_USER_SETTINGS_VERSION so existing settings files get its default.
// hotkey_actions_build and app_check_key_combinations, and bind it in both
// assets/data/default_hotkeys files. Settings files at APP_USER_SETTINGS_VERSION get its default
// binding at the next start (hotkeys_missing_lines); raising the version would give them "none".
program_exit_hotkey: Hotkey;
minimize_to_tray_hotkey: Hotkey;
toggle_debug_panel_hotkey: Hotkey;
@ -162,6 +163,10 @@ increase_font_size_hotkey: Hotkey;
reset_window_hotkey: Hotkey;
reset_layout_hotkey: Hotkey;
// The selected capture (app/viewer.jai). Not while typing, so Ctrl-C in the search box copies its text.
copy_image_hotkey: Hotkey = .{ignore_when_text_field_active = true};
copy_path_hotkey: Hotkey = .{ignore_when_text_field_active = true};
Hotkey_Action :: struct {
name: string; // as written in [hotkeys]
hotkey: *Hotkey;
@ -182,6 +187,8 @@ hotkey_actions_build :: (out: *[..] Hotkey_Action) {
add(out, "increase_font_size", *increase_font_size_hotkey);
add(out, "reset_window", *reset_window_hotkey);
add(out, "reset_layout", *reset_layout_hotkey);
add(out, "copy_image", *copy_image_hotkey);
add(out, "copy_path", *copy_path_hotkey);
}
hotkey_actions: [..] Hotkey_Action;
@ -627,6 +634,9 @@ app_check_key_combinations :: () {
else toast_show(.Error, "Could not reset the window");
}
if check_key_combination(*reset_layout_hotkey) imgui_reset_layout_requested = true; // applied between frames
// The viewer is the shutdown thread's once shutdown has started.
if check_key_combination(*copy_image_hotkey) && !app_exit_shutdown_started() viewer_copy_image();
if check_key_combination(*copy_path_hotkey) && !app_exit_shutdown_started() viewer_copy_path();
if should_exit_this_frame {
log("Calling exit hotkey... exiting program!");