mcap/src/app/app_ui.jai
Musa Mahmood 775c2873e4 [0.0.10] Shutdown: say what it is waiting on, stop OCR mid-batch, no race with the frame loop
- The exit overlay says which step of shutdown is running and what it waits on ("Stopping the OCR
  scan: finishing the text lines it is reading.", the import, the viewer, the models), and how long
  that step has taken once it passes 2 s. It said "Saving..." for all of it.
- OCR stops mid-batch: ocr_process takes a cancel flag (Ocr_Engine.cancel), checked by its workers
  before each piece of work; a scan sets it to its own cancel. A cut-short batch stores nothing and
  counts no failures. Closing during a scan took minutes when a batch held large images; now well
  under a second.
- Once shutdown starts, the main thread stops running the app's frames (the viewer, search, import,
  OCR, Stats) and draws only the overlay: background_frame read the search thread group while the
  shutdown thread was tearing it down, an access violation on exit.
2026-10-09 23:37:35 +01:00

93 lines
4.0 KiB
Plaintext

// The app's own UI: the capture viewer (app/viewer.jai). Everything else in src/ is the shell
// around it: the window, the frame loop, the tray, hotkeys, settings, themes, logging and the
// debug panels.
APP_MAIN_WINDOW_LABEL :: "Main";
// Once, after ImGui, the fonts, the theme and the settings are up and before the first frame.
app_ui_startup :: () {
viewer_startup();
import_startup();
ocr_ui_startup();
labels_ui_startup();
}
// Every frame, before input is handed to ImGui. Poll background work here.
app_ui_frame_start :: () {
}
// Once, on the shutdown thread after the window is asked to close (exit_shutdown.jai): save and
// stop background work here. Must not touch ImGui, since the main thread is still drawing frames
// (the overlay; app_main_window_ui stops running the app's frames once shutdown starts).
app_ui_shutdown :: () {
app_shutdown_step(.IMPORT);
import_shutdown(); // before the viewer: it hands the viewer what it writes
app_shutdown_step(.VIEWER);
viewer_shutdown();
app_shutdown_step(.OCR);
ocr_ui_shutdown();
app_shutdown_step(.LABELS);
labels_ui_shutdown(); // after the OCR thread and the background workers have stopped
app_shutdown_step(.STATS);
stats_ui_shutdown();
}
// Which step of app_ui_shutdown is running, and since when, for the overlay a slow shutdown shows.
App_Shutdown_Step :: enum s32 { NONE; IMPORT; VIEWER; OCR; LABELS; STATS; }
app_shutdown: struct {
step: App_Shutdown_Step;
step_started_ns: s64; // monotonic
}
app_shutdown_step :: (step: App_Shutdown_Step) {
atomic_write(*app_shutdown.step_started_ns, to_nanoseconds(current_time_monotonic()));
atomic_write(*app_shutdown.step, step);
}
// Main thread, while app_ui_shutdown runs: what it is waiting on, as a line or two for the overlay
// (temp); "" before it has started a step.
app_ui_shutdown_status :: () -> string {
step := atomic_read(*app_shutdown.step);
text: string;
if step == {
case .NONE; return "";
case .IMPORT;
if importer.phase == .INDEXING text = "Stopping the import: it is indexing the library, and stops after the files it is on.";
else if importer.running text = "Stopping the import: finishing the files it is on (a large video takes a while to copy).";
else text = "Stopping the import.";
case .VIEWER;
text = "Stopping the thumbnail, catalog and search workers";
if viewer.open_started text = tprint("%, and writing out % to play", text, viewer.open_name);
text = tprint("%.", text);
case .OCR;
text = "Closing the OCR model.";
if ocr_ui.thread_busy if ocr_ui.task.kind == {
case .SCAN; text = "Stopping the OCR scan: finishing the text lines it is reading.";
case .ERASE; text = "Stopping the erase: finishing the capture it is on.";
case .READ; text = "Waiting for OCR to finish reading a capture.";
case .CREATE; text = "Waiting for the OCR model to finish loading.";
case .LABELS_CREATE; text = "Waiting for the label model to finish loading.";
case .LABEL_SET; text = "Waiting for the label list to finish.";
case; text = "Stopping OCR.";
}
case .LABELS; text = "Closing the label model.";
case .STATS; text = "Closing Stats.";
}
waited := cast(float64) (to_nanoseconds(current_time_monotonic()) - atomic_read(*app_shutdown.step_started_ns)) / 1_000_000_000.0;
if waited >= 2 text = tprint("%\n(this step: % s)", text, FormatFloat.{value = waited, trailing_width = 0, zero_removal = .NO});
return text;
}
app_main_window_ui :: () {
push_imgui_window(APP_MAIN_WINDOW_LABEL);
// Once shutdown has started, its thread is stopping the threads these frames hand work to and take
// it from (the viewer's workers, the search group, the import, OCR), and freeing what they share:
// running them now would race it. The exit overlay says what it is waiting on.
if app_exit_shutdown_started() return;
viewer_ui();
ocr_ui_frame();
labels_ui_frame();
stats_ui_frame();
import_frame();
}