diff --git a/check_targets.jai b/check_targets.jai index 4865715..aa871ea 100644 --- a/check_targets.jai +++ b/check_targets.jai @@ -29,7 +29,7 @@ APP_NAME :: "mcap"; APP_FOLDER_NAME :: "mcap"; GIT_BRANCH :: "check"; GIT_HASH :: "-"; -VERSION :: "0.1.12"; +VERSION :: "0.1.13"; BUILD_DATE :: "-"; DEBUG :: true; COMPILE_TESTS :: true; diff --git a/first.jai b/first.jai index e244721..9617731 100644 --- a/first.jai +++ b/first.jai @@ -8,7 +8,7 @@ APP_NAME :: "mcap"; APP_FOLDER_NAME :: "mcap"; APP_BUNDLE_IDENTIFIER :: "com.example.mcap"; -VERSION :: "0.1.12"; +VERSION :: "0.1.13"; // 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"; diff --git a/src/app/app_ui.jai b/src/app/app_ui.jai index 4784ec8..106ade7 100644 --- a/src/app/app_ui.jai +++ b/src/app/app_ui.jai @@ -17,6 +17,7 @@ app_ui_startup :: () { // Every frame, before input is handed to ImGui. Poll background work here. app_ui_frame_start :: () { screen_capture_frame(); // also while hidden in the tray, where the frame ends before the UI + ocr_ui_unload_idle_models(); // models unused for a while, also while in the tray } // Once, on the shutdown thread after the window is asked to close (exit_shutdown.jai): save and @@ -77,6 +78,7 @@ app_ui_shutdown_status :: () -> string { case .LABELS_CREATE; text = "Waiting for the label model to finish loading."; case .LABEL_SET; text = "Waiting for the label list to finish."; case .SEGMENT; text = "Stopping the segmentation scan: finishing the images it is segmenting."; + case .UNLOAD; text = "Waiting for the models unused for a while to unload."; case; text = "Stopping OCR."; } case .SEGMENT; text = "Closing the segmentation model."; diff --git a/src/app/background.jai b/src/app/background.jai index fe322b3..3d90686 100644 --- a/src/app/background.jai +++ b/src/app/background.jai @@ -149,6 +149,7 @@ background_search_start :: () { likes: [..] string; likes.allocator = temp; for request.terms if it.field == .LIKE array_add(*likes, it.text); + if likes.count labels_ui_want(); // when it is not up, the search is worked out again once it is if likes.count && labels_ui.engine { request.like_texts = NewArray(likes.count, string); for likes request.like_texts[it_index] = copy_string(it); diff --git a/src/app/import.jai b/src/app/import.jai index 04860f2..07b5d2e 100644 --- a/src/app/import.jai +++ b/src/app/import.jai @@ -921,7 +921,7 @@ import_frame :: () { using ImGui; importer.scan_pending = importer.read_after && importer.new_indices.count > 0 && !app.import_source; importer.segment_pending = library_folder_segments(importer.dest_name) && importer.new_indices.count > 0 && !app.import_source; } - if importer.scan_pending && !ocr_ui.thread_busy && ocr_ui.engine && labels_ui_settled() { + if importer.scan_pending && ocr_ui_models_ready(labels_ui.scan_labels) && ocr_ui.engine { importer.scan_pending = false; ocr_scan_start(importer.new_indices); } diff --git a/src/app/labels_ui.jai b/src/app/labels_ui.jai index 4345c47..9f2a365 100644 --- a/src/app/labels_ui.jai +++ b/src/app/labels_ui.jai @@ -7,9 +7,10 @@ // vectors and a hash of it, made on the OCR thread (app/ocr_ui.jai) and never changed after, so a // scan can hold the set it started with while the list is edited. // -// The engine is made on the OCR thread too, once, after the OCR engine; it is destroyed only at -// shutdown, after the background workers (app/background.jai), which use its text model for like: -// searches, have stopped. +// The engine is made on the OCR thread too, when a reading, a scan or a like: search first needs it +// (ocr_ui_models_ready, labels_ui_want), and destroyed there once it has gone unused for a while +// (ocr_ui_unload_idle_models) with no search job holding it; else at shutdown, after the background +// workers (app/background.jai), which use its text model for like: searches, have stopped. LABELS_WINDOW :: "Labels"; LABELS_MODEL :: "siglip2-base-patch16-384"; // the one label model; its chunks are keyed by this @@ -29,8 +30,10 @@ labels_ui: struct { // Owned by the OCR thread while ocr_ui.thread_busy, like ocr_ui.engine. engine: *Labels_Engine; engine_error: string; - engine_tried: bool; - set: *Label_Set; // the newest list's + engine_tried: bool; // made, or failed; cleared when it is unloaded + last_used: Apollo_Time; // main thread: when a task or a like: search last used it + wanted: bool; // main thread: a like: search asked for it + set: *Label_Set; // the newest list's retired_sets: [..] *Label_Set; // replaced while a scan might hold them; freed when the OCR thread is idle // Main thread. @@ -77,11 +80,11 @@ labels_ui_shutdown :: () { free(labels_ui.counted_hash,, context.default_allocator); } -// Whether a scan can start with labels: the engine has been tried and, when it is up, the list's -// set is made. -labels_ui_settled :: () -> bool { - if ocr_ui.thread_busy || !labels_ui.engine_tried return false; - return !labels_ui.engine || (labels_ui.set && labels_ui.set.hash == labels_ui.list_hash); +// A like: search needs the engine: it is made when the OCR thread is free (labels_ui_frame), and the +// search worked out again once it is up. +labels_ui_want :: () { + labels_ui.last_used = current_time_monotonic(); + if !labels_ui.engine_tried labels_ui.wanted = true; } // ---------------------------------------------------------------- the label list @@ -218,8 +221,9 @@ labels_ui_frame :: () { using ImGui; if !ocr_ui.thread_busy { for labels_ui.retired_sets label_set_free(it); array_reset_keeping_memory(*labels_ui.retired_sets); - if !labels_ui.engine_tried && ocr_ui.engine_tried { // after the OCR engine - ocr_ui_start(.{kind = .LABELS_CREATE}); + if labels_ui.wanted { + labels_ui.wanted = false; + if !labels_ui.engine_tried ocr_ui_start(.{kind = .LABELS_CREATE}); } else if labels_ui.engine && (!labels_ui.set || labels_ui.set.hash != labels_ui.list_hash) && labels_ui.list.count { ocr_ui_start(.{kind = .LABEL_SET}); } @@ -272,7 +276,7 @@ labels_window_contents :: () { using ImGui; Text("%: images on %, text on the CPU", labels_ui.engine.model.title, ifx in_use then "the GPU (CUDA)" else "the CPU"); if explanation text_with_links(explanation); } else if !labels_ui.engine_tried { - TextColored(app_theme.palette.dim, "Starts after OCR."); + TextColored(app_theme.palette.dim, "Not loaded: it loads when a capture is read or labelled, or a like: search runs, and unloads after % minutes unused.", MODELS_IDLE_UNLOAD_SECONDS / 60); } // How much of the library is labelled, and with which list: counted again only when the catalog diff --git a/src/app/ocr_ui.jai b/src/app/ocr_ui.jai index 37a07a8..98e6671 100644 --- a/src/app/ocr_ui.jai +++ b/src/app/ocr_ui.jai @@ -10,11 +10,15 @@ // Making the engines (OCR, then labels), the label list's vectors, reading one capture and the scan // all run on one background thread, one at a time; the UI only looks at what it has finished, and // at the scan's progress counters. +// +// No engine is made at startup: each is made when something first needs it (ocr_ui_models_ready), +// and destroyed again once it has gone MODELS_IDLE_UNLOAD_SECONDS unused (ocr_ui_unload_idle_models). OCR_WINDOW :: "OCR"; OCR_DEFAULT_MODEL :: "ppocr-v5-mobile"; OCR_SCAN_BATCH :: 8; // captures per ocr_process call in a scan OCR_STORE_ATTEMPTS :: 4; // a file the viewer has open at that moment is tried again, 250 ms apart +MODELS_IDLE_UNLOAD_SECONDS :: 10 * 60; // an engine unused this long is destroyed, giving its memory back ocr_ui: struct { models: [] Ocr_Model_Info; @@ -24,7 +28,8 @@ ocr_ui: struct { // Owned by the main thread while thread_busy is false; by the background thread while it is true. engine: *Ocr_Engine; engine_error: string; // why the last engine could not be made - engine_tried: bool; // the first engine has been made, or failed: labels start after it + engine_tried: bool; // the engine has been made, or failed; cleared when it is unloaded + last_used: Apollo_Time; // main thread: when a task last used the engine thread: Thread; thread_started: bool; thread_busy: bool; // set by the main thread, cleared by the background one @@ -33,6 +38,10 @@ ocr_ui: struct { read_error: string; // why the last single read failed read_index: s32 = -1; read_seconds: float64; + read_wanted: bool; // the OCR button was pressed: it reads once the models are up + read_wanted_index: s32; + read_wanted_path: string; // heap + scan_wanted: bool; // the scan button was pressed: it scans once the models are up scan: Ocr_Scan; scan_requested: bool; // -scan has started its scan @@ -49,7 +58,7 @@ ocr_ui: struct { toast_shown := false; } -Ocr_Task_Kind :: enum { NONE; CREATE; READ; SCAN; LABELS_CREATE; LABEL_SET; ERASE; SEGMENT; } +Ocr_Task_Kind :: enum { NONE; CREATE; READ; SCAN; LABELS_CREATE; LABEL_SET; ERASE; SEGMENT; UNLOAD; } Ocr_Task :: struct { kind: Ocr_Task_Kind; @@ -59,6 +68,10 @@ Ocr_Task :: struct { // LABELS_CREATE and LABEL_SET: the label list as it was when the task started (heap). label_names, label_prompts: [] string; label_hash: string; + // UNLOAD: the engines to destroy, already taken out of ocr_ui, labels_ui and segment_ui. + unload_ocr: *Ocr_Engine; + unload_labels: *Labels_Engine; + unload_segment: *Segment_Engine; } // The library scan. The counters and flags are shared with the background thread. @@ -103,7 +116,7 @@ ocr_ui_startup :: () { ocr_ui.models = models; if error ocr_ui.models_error = copy_string(error); ocr_ui.model_index = ocr_model_index(app_user_settings.ocr_model); - if ocr_ui.models.count ocr_ui_start(.{kind = .CREATE, model = ocr_ui.models[ocr_ui.model_index].name}); + // The engine is made when something first needs it: ocr_ui_models_ready. } ocr_ui_shutdown :: () { @@ -111,6 +124,7 @@ ocr_ui_shutdown :: () { atomic_swap(*ocr_ui.scan.paused, false); if ocr_ui.thread_started thread_is_done(*ocr_ui.thread, -1); ocr_destroy(ocr_ui.engine); + free(ocr_ui.read_wanted_path,, context.default_allocator); } // The model the settings name, or the default one, or the first; with a log line when the settings @@ -135,7 +149,13 @@ ocr_ui_choose_model :: (index: s32, save := true) { if ocr_ui.thread_busy return; ocr_ui.model_index = index; ocr_ui.toast_shown = true; // said once already, or not needed - ocr_ui_start(.{kind = .CREATE, model = ocr_ui.models[index].name}); + if ocr_ui.engine { + ocr_ui_start(.{kind = .CREATE, model = ocr_ui.models[index].name}); + } else { // not loaded, or failed: the next use makes the one chosen + ocr_ui.engine_tried = false; + free(ocr_ui.engine_error,, context.default_allocator); + ocr_ui.engine_error = ""; + } if save { free(app_user_settings.ocr_model,, context.default_allocator); app_user_settings.ocr_model = copy_string(ocr_ui.models[index].name,, context.default_allocator); @@ -160,6 +180,7 @@ ocr_ui_start :: (task: Ocr_Task) -> bool { } ocr_ui.task.label_hash = copy_string(labels_ui.list_hash); } + ocr_ui_note_use(task.kind, current_time_monotonic()); ocr_ui.thread_busy = true; if ocr_ui.thread_started thread_deinit(*ocr_ui.thread); ocr_ui.thread_started = thread_init(*ocr_ui.thread, ocr_ui_thread_proc, temporary_storage_size = 4 * 1024 * 1024, name = "ocr"); @@ -168,6 +189,86 @@ ocr_ui_start :: (task: Ocr_Task) -> bool { return true; } +// ---------------------------------------------------------------- the models: made when needed, unloaded when idle + +// Whether the OCR engine (and, with labels, the label engine and the list's set) are up, or could +// not be made, so a reading or a scan can start. When not, starts making the next of them on the OCR +// thread, if it is free. Called each frame by what waits on them. After it returns true, a null +// ocr_ui.engine means OCR could not start. +ocr_ui_models_ready :: (labels := true) -> bool { + if ocr_ui.thread_busy return false; + if !ocr_ui.engine_tried { + if !ocr_ui.models.count return true; // ocr_ui.models_error says why + ocr_ui_start(.{kind = .CREATE, model = ocr_ui.models[ocr_ui.model_index].name}); + return false; + } + if !labels return true; + if !labels_ui.engine_tried { + ocr_ui_start(.{kind = .LABELS_CREATE}); + return false; + } + if labels_ui.engine && labels_ui.list.count && (!labels_ui.set || labels_ui.set.hash != labels_ui.list_hash) { + ocr_ui_start(.{kind = .LABEL_SET}); + return false; + } + return true; +} + +// Each frame, also while the window is in the tray: notes which engines the OCR thread's task uses, +// and destroys those unused for MODELS_IDLE_UNLOAD_SECONDS, on the OCR thread. +ocr_ui_unload_idle_models :: () { + if app_exit_shutdown_started() return; // the shutdown thread destroys them + now := current_time_monotonic(); + if ocr_ui.thread_busy { ocr_ui_note_use(ocr_ui.task.kind, now); return; } + if ocr_ui.scan.running || ocr_ui.read_wanted || ocr_ui.scan_wanted return; + + idle :: (last_used: Apollo_Time, now: Apollo_Time) -> bool { + return to_float64_seconds(now - last_used) >= MODELS_IDLE_UNLOAD_SECONDS; + } + task := Ocr_Task.{kind = .UNLOAD}; + if ocr_ui.engine && idle(ocr_ui.last_used, now) { + task.unload_ocr = ocr_ui.engine; + ocr_ui.engine = null; + ocr_ui.engine_tried = false; + } + // A like: search's jobs hold the label engine until they are back. + if labels_ui.engine && idle(labels_ui.last_used, now) && !background.jobs_out { + task.unload_labels = labels_ui.engine; + labels_ui.engine = null; + labels_ui.engine_tried = false; + labels_ui.search_ready = false; // the list's set stays: the same model makes the same vectors + } + if segment_ui.engine && idle(segment_ui.last_used, now) { + task.unload_segment = segment_ui.engine; + segment_ui.engine = null; + } + if !task.unload_ocr && !task.unload_labels && !task.unload_segment return; + + names: [..] string; + names.allocator = temp; + if task.unload_ocr array_add(*names, task.unload_ocr.model.name); + if task.unload_labels array_add(*names, LABELS_MODEL); + if task.unload_segment array_add(*names, SEGMENT_MODEL); + log("[Models] Unloading % after % minutes unused.", join(..names, separator = ", ",, temp), MODELS_IDLE_UNLOAD_SECONDS / 60); + if !ocr_ui_start(task) { // the thread could not start: destroy them here instead + ocr_destroy(task.unload_ocr); + labels_destroy(task.unload_labels); + segment_destroy(task.unload_segment); + } +} + +// The engines a task of this kind uses, as used at now: when it starts, and each frame it runs. +ocr_ui_note_use :: (kind: Ocr_Task_Kind, now: Apollo_Time) { + if kind == { + case .CREATE; ocr_ui.last_used = now; + case .LABELS_CREATE; labels_ui.last_used = now; + case .LABEL_SET; labels_ui.last_used = now; + case .READ; ocr_ui.last_used = now; labels_ui.last_used = now; + case .SCAN; ocr_ui.last_used = now; if ocr_ui.scan.label_set labels_ui.last_used = now; + case .SEGMENT; segment_ui.last_used = now; + } +} + // ---------------------------------------------------------------- the background thread ocr_ui_thread_proc :: (thread: *Thread) -> s64 { @@ -229,6 +330,11 @@ ocr_ui_thread_proc :: (thread: *Thread) -> s64 { case .SEGMENT; segment_ui_scan_task(); // app/segment_ui.jai + case .UNLOAD; + ocr_destroy(task.unload_ocr); + labels_destroy(task.unload_labels); + segment_destroy(task.unload_segment); + case .ERASE; scan := *ocr_ui.scan; // Not in temporary storage, which is reset after each capture. @@ -512,7 +618,8 @@ ocr_ui_scan_and_exit :: (busy: bool) { if ocr_ui.models_error { finish(1, "[OCR scan] %", ocr_ui.models_error); return; } if viewer.scan_error { finish(1, "[OCR scan] %", viewer.scan_error); return; } if !ocr_ui.scan_requested { - if busy || !ocr_ui.engine || viewer.catalog_loaded < viewer.captures.count || !labels_ui_settled() return; + if busy || viewer.catalog_loaded < viewer.captures.count || !ocr_ui_models_ready(labels_ui.scan_labels) return; + if !ocr_ui.engine { finish(1, "[OCR scan] OCR could not start: %", ocr_ui.engine_error); return; } ocr_ui.scan_requested = true; ocr_ui.window_open = true; // its progress bar ocr_scan_start(); @@ -570,17 +677,18 @@ ocr_scan_ui :: () { using ImGui; catalog_ready := viewer.catalog_loaded >= viewer.captures.count; if !scan.running { - can_scan := ocr_ui.engine && !busy && catalog_ready; + can_scan := !busy && !ocr_ui.scan_wanted && catalog_ready; if !can_scan BeginDisabled(); label := ifx viewer.search_terms.count then "Scan the captures shown" else "Scan the library"; - if Button(temp_c_string(label)) ocr_scan_start(); + if Button(temp_c_string(label)) ocr_ui.scan_wanted = true; // it starts once the models are up: ocr_ui_frame if !can_scan EndDisabled(); SameLine(); - labels_ready := labels_ui.engine && labels_ui.set && labels_ui.set.hash == labels_ui.list_hash; - if !labels_ready BeginDisabled(); + labels_failed := labels_ui.engine_tried && !labels_ui.engine; + if labels_failed BeginDisabled(); Checkbox("Labels too", *labels_ui.scan_labels); - if !labels_ready EndDisabled(); - if IsItemHovered(.AllowWhenDisabled) SetTooltip(ifx labels_ready then tprint("Also labels each capture with %, storing its labels and image vector", LABELS_MODEL) else "Labelling is not ready: see the Labels window"); + if labels_failed EndDisabled(); + if IsItemHovered(.AllowWhenDisabled) SetTooltip(ifx labels_failed then "Labelling could not start: see the Labels window" else tprint("Also labels each capture with %, storing its labels and image vector", LABELS_MODEL)); + if ocr_ui.scan_wanted TextColored(app_theme.palette.dim, "Loading the models..."); if !catalog_ready TextColored(app_theme.palette.dim, "(once the catalog has loaded)"); else TextColored(app_theme.palette.dim, "Reads every capture this model has not read, and labels those not yet labelled with this list. Each is stored as it is read, so a scan can stop and start again."); if scan.total && scan.done { @@ -652,6 +760,17 @@ ocr_ui_frame :: () { using ImGui; if app.scan_and_exit ocr_ui_scan_and_exit(busy); if app.erase_and_exit ocr_ui_erase_and_exit(busy); ocr_erase_popup(); + // The OCR and scan buttons wait for the models they load. + if ocr_ui.read_wanted && ocr_ui_models_ready() { + ocr_ui.read_wanted = false; + ocr_ui_start(.{kind = .READ, path = ocr_ui.read_wanted_path, index = ocr_ui.read_wanted_index}); + busy = ocr_ui.thread_busy; + } + if ocr_ui.scan_wanted && ocr_ui_models_ready(labels_ui.scan_labels) { + ocr_ui.scan_wanted = false; + if ocr_ui.engine ocr_scan_start(); + busy = ocr_ui.thread_busy; + } if !busy && ocr_ui.engine && !ocr_ui.toast_shown { ocr_ui.toast_shown = true; in_use, explanation := ocr_gpu_status(ocr_ui.engine); @@ -685,7 +804,11 @@ ocr_ui_frame :: () { using ImGui; } else if ocr_ui.engine_error { TextColored(app_theme.palette.warning, "OCR could not start:"); TextWrapped("%", ocr_ui.engine_error); - } else if ocr_ui.engine { + } else if !ocr_ui.engine { + TextColored(app_theme.palette.dim, "Not loaded: it loads when a capture is read or a scan starts, and unloads after % minutes unused.", MODELS_IDLE_UNLOAD_SECONDS / 60); + Separator(); + ocr_scan_ui(); + } else { in_use, explanation := ocr_gpu_status(ocr_ui.engine); Text("Running on: %", ifx in_use then "the GPU (CUDA)" else "the CPU"); SameLine(); @@ -750,12 +873,21 @@ ocr_ui_capture_controls :: (index: s32, path: string) { using ImGui; model := ifx ocr_ui.models.count then ocr_ui.models[ocr_ui.model_index].name else ""; has_reading := entry && catalog_section(entry, model) != null; - can_read := ocr_ui.engine && !busy; + ocr_failed := ocr_ui.engine_tried && !ocr_ui.engine; + can_read := ocr_ui.models.count && !busy && !ocr_ui.read_wanted && !ocr_failed; if !can_read BeginDisabled(); - if SmallButton(temp_c_string(ifx has_reading then "Read again" else "OCR")) ocr_ui_start(.{kind = .READ, path = path, index = index}); + if SmallButton(temp_c_string(ifx has_reading then "Read again" else "OCR")) { // it reads once the models are up: ocr_ui_frame + ocr_ui.read_wanted = true; + ocr_ui.read_wanted_index = index; + free(ocr_ui.read_wanted_path,, context.default_allocator); + ocr_ui.read_wanted_path = copy_string(path,, context.default_allocator); + } if !can_read EndDisabled(); SameLine(); - if busy && ocr_ui.task.kind == .READ && ocr_ui.task.index == index { + if ocr_ui.read_wanted && ocr_ui.read_wanted_index == index { + TextColored(app_theme.palette.dim, "Loading the models..."); + SameLine(); + } else if busy && ocr_ui.task.kind == .READ && ocr_ui.task.index == index { TextColored(app_theme.palette.dim, "Reading..."); SameLine(); } else if !busy && ocr_ui.read_index == index { @@ -789,8 +921,8 @@ ocr_ui_capture_controls :: (index: s32, path: string) { using ImGui; } } SameLine(); - } else if !ocr_ui.engine { - TextColored(app_theme.palette.dim, "(OCR is not running; see the OCR window)"); + } else if ocr_failed { + TextColored(app_theme.palette.dim, "(OCR could not start; see the OCR window)"); SameLine(); } if SmallButton("OCR window") ocr_ui.window_open = true; diff --git a/src/app/screen_capture.jai b/src/app/screen_capture.jai index 86740cf..b35d4c1 100644 --- a/src/app/screen_capture.jai +++ b/src/app/screen_capture.jai @@ -112,8 +112,8 @@ screen_capture_frame :: () { remove job; } - // As an import's are, once the models are free: OCR and labels, then segmentation. - if screen_capture.scan_pending.count && !ocr_ui.thread_busy && !ocr_ui.scan.running && ocr_ui.engine && labels_ui_settled() { + // As an import's are, once the models are loaded and free: OCR and labels, then segmentation. + if screen_capture.scan_pending.count && !ocr_ui.scan.running && ocr_ui_models_ready(labels_ui.scan_labels) && ocr_ui.engine { ocr_scan_start(screen_capture.scan_pending); array_reset_keeping_memory(*screen_capture.scan_pending); } diff --git a/src/app/segment_ui.jai b/src/app/segment_ui.jai index 4dc891c..8455e32 100644 --- a/src/app/segment_ui.jai +++ b/src/app/segment_ui.jai @@ -6,6 +6,7 @@ // The scan runs on the OCR thread (app/ocr_ui.jai), as a task of its own, so it shares the OCR scan's // progress, pause and cancel, and one thread uses the GPU at a time. The engine is made by the first // scan, not at startup: it takes some 2.4 GB of GPU memory, and most sessions never segment anything. +// It is destroyed again once it has gone unused for a while (ocr_ui_unload_idle_models). SEGMENT_WINDOW :: "Segmentation"; SEGMENT_MODEL :: "mask2former-swin-base-coco-panoptic"; // the one segmentation model; its chunks are keyed by this @@ -15,6 +16,7 @@ segment_ui: struct { // Owned by the OCR thread while ocr_ui.thread_busy, like ocr_ui.engine. engine: *Segment_Engine; engine_error: string; + last_used: Apollo_Time; // main thread: when a scan last used the engine (ocr_ui_unload_idle_models) // Main thread. window_open: bool; @@ -290,8 +292,8 @@ segment_ui_segment_and_exit :: (busy: bool) { } if viewer.scan_error { finish(1, "[Segment scan] %", viewer.scan_error); return; } if !segment_ui.scan_requested { - // After the catalog, and after the engines made at startup, which use the same thread. - if busy || viewer.catalog_loaded < viewer.captures.count || !ocr_ui.engine_tried || !labels_ui.engine_tried return; + // After the catalog. + if busy || viewer.catalog_loaded < viewer.captures.count return; segment_ui.scan_requested = true; segment_ui.window_open = true; // its progress bar if !segment_scan_start(quiet = true) finish(0, "[Segment scan] -segment: nothing to segment (no image in a folder set to segment needs it)."); diff --git a/src/app/viewer.jai b/src/app/viewer.jai index f8de62a..39c93da 100644 --- a/src/app/viewer.jai +++ b/src/app/viewer.jai @@ -908,7 +908,7 @@ viewer_ui :: () { using ImGui; engine: *Ocr_Engine; if !ocr_ui.thread_busy engine = ocr_ui.engine; // the background thread owns it while busy if engine SetTooltip("OCR with %, on %", engine.model.name, ifx engine.provider == .CUDA then "the GPU" else "the CPU"); - else SetTooltip("OCR is starting, or could not start; the OCR window says which"); + else SetTooltip("OCR is not loaded, is loading, or could not start; the OCR window says which"); } // A running OCR scan, as a thin bar under the search, so it shows with the OCR window closed.