[0.1.13] Models load when first needed, and unload after 10 minutes unused
- No model is made at startup (private memory there falls from about 4.3 GB to about 420 MB). ocr_ui_models_ready makes the OCR engine, then the label engine and the list's set, on the OCR thread when a reading, a scan (the button, -scan, or after a capture or an import) needs them; the OCR and scan buttons wait for them, showing "Loading the models...". A like: search asks for the label engine alone (labels_ui_want) and is worked out again once it is up. - ocr_ui_unload_idle_models, each frame and also in the tray: an engine (OCR, labels or segmentation) unused for MODELS_IDLE_UNLOAD_SECONDS (10 min) is destroyed by an UNLOAD task on the OCR thread, never during a scan, a waiting read or scan, or while search jobs hold the label engine. The label list's set stays, so loading again does not score the prompts again. - Choosing another OCR model with none loaded only notes it; -segment no longer waits for the OCR and label engines. Some 1.4 GB stays after an unload, the CUDA runtime it loaded.
This commit is contained in:
parent
e7229343c7
commit
157888d6d8
@ -29,7 +29,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.12";
|
VERSION :: "0.1.13";
|
||||||
BUILD_DATE :: "-";
|
BUILD_DATE :: "-";
|
||||||
DEBUG :: true;
|
DEBUG :: true;
|
||||||
COMPILE_TESTS :: true;
|
COMPILE_TESTS :: true;
|
||||||
|
|||||||
@ -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.12";
|
VERSION :: "0.1.13";
|
||||||
// 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";
|
||||||
|
|||||||
@ -17,6 +17,7 @@ app_ui_startup :: () {
|
|||||||
// Every frame, before input is handed to ImGui. Poll background work here.
|
// Every frame, before input is handed to ImGui. Poll background work here.
|
||||||
app_ui_frame_start :: () {
|
app_ui_frame_start :: () {
|
||||||
screen_capture_frame(); // also while hidden in the tray, where the frame ends before the UI
|
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
|
// 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 .LABELS_CREATE; text = "Waiting for the label model to finish loading.";
|
||||||
case .LABEL_SET; text = "Waiting for the label list to finish.";
|
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 .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; text = "Stopping OCR.";
|
||||||
}
|
}
|
||||||
case .SEGMENT; text = "Closing the segmentation model.";
|
case .SEGMENT; text = "Closing the segmentation model.";
|
||||||
|
|||||||
@ -149,6 +149,7 @@ background_search_start :: () {
|
|||||||
likes: [..] string;
|
likes: [..] string;
|
||||||
likes.allocator = temp;
|
likes.allocator = temp;
|
||||||
for request.terms if it.field == .LIKE array_add(*likes, it.text);
|
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 {
|
if likes.count && labels_ui.engine {
|
||||||
request.like_texts = NewArray(likes.count, string);
|
request.like_texts = NewArray(likes.count, string);
|
||||||
for likes request.like_texts[it_index] = copy_string(it);
|
for likes request.like_texts[it_index] = copy_string(it);
|
||||||
|
|||||||
@ -921,7 +921,7 @@ import_frame :: () { using ImGui;
|
|||||||
importer.scan_pending = importer.read_after && importer.new_indices.count > 0 && !app.import_source;
|
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;
|
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;
|
importer.scan_pending = false;
|
||||||
ocr_scan_start(importer.new_indices);
|
ocr_scan_start(importer.new_indices);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
// 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.
|
// 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
|
// The engine is made on the OCR thread too, when a reading, a scan or a like: search first needs it
|
||||||
// shutdown, after the background workers (app/background.jai), which use its text model for like:
|
// (ocr_ui_models_ready, labels_ui_want), and destroyed there once it has gone unused for a while
|
||||||
// searches, have stopped.
|
// (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_WINDOW :: "Labels";
|
||||||
LABELS_MODEL :: "siglip2-base-patch16-384"; // the one label model; its chunks are keyed by this
|
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.
|
// Owned by the OCR thread while ocr_ui.thread_busy, like ocr_ui.engine.
|
||||||
engine: *Labels_Engine;
|
engine: *Labels_Engine;
|
||||||
engine_error: string;
|
engine_error: string;
|
||||||
engine_tried: bool;
|
engine_tried: bool; // made, or failed; cleared when it is unloaded
|
||||||
set: *Label_Set; // the newest list's
|
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
|
retired_sets: [..] *Label_Set; // replaced while a scan might hold them; freed when the OCR thread is idle
|
||||||
|
|
||||||
// Main thread.
|
// Main thread.
|
||||||
@ -77,11 +80,11 @@ labels_ui_shutdown :: () {
|
|||||||
free(labels_ui.counted_hash,, context.default_allocator);
|
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
|
// A like: search needs the engine: it is made when the OCR thread is free (labels_ui_frame), and the
|
||||||
// set is made.
|
// search worked out again once it is up.
|
||||||
labels_ui_settled :: () -> bool {
|
labels_ui_want :: () {
|
||||||
if ocr_ui.thread_busy || !labels_ui.engine_tried return false;
|
labels_ui.last_used = current_time_monotonic();
|
||||||
return !labels_ui.engine || (labels_ui.set && labels_ui.set.hash == labels_ui.list_hash);
|
if !labels_ui.engine_tried labels_ui.wanted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- the label list
|
// ---------------------------------------------------------------- the label list
|
||||||
@ -218,8 +221,9 @@ labels_ui_frame :: () { using ImGui;
|
|||||||
if !ocr_ui.thread_busy {
|
if !ocr_ui.thread_busy {
|
||||||
for labels_ui.retired_sets label_set_free(it);
|
for labels_ui.retired_sets label_set_free(it);
|
||||||
array_reset_keeping_memory(*labels_ui.retired_sets);
|
array_reset_keeping_memory(*labels_ui.retired_sets);
|
||||||
if !labels_ui.engine_tried && ocr_ui.engine_tried { // after the OCR engine
|
if labels_ui.wanted {
|
||||||
ocr_ui_start(.{kind = .LABELS_CREATE});
|
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 {
|
} 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});
|
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");
|
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);
|
if explanation text_with_links(explanation);
|
||||||
} else if !labels_ui.engine_tried {
|
} 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
|
// How much of the library is labelled, and with which list: counted again only when the catalog
|
||||||
|
|||||||
@ -10,11 +10,15 @@
|
|||||||
// Making the engines (OCR, then labels), the label list's vectors, reading one capture and the scan
|
// 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
|
// 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.
|
// 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_WINDOW :: "OCR";
|
||||||
OCR_DEFAULT_MODEL :: "ppocr-v5-mobile";
|
OCR_DEFAULT_MODEL :: "ppocr-v5-mobile";
|
||||||
OCR_SCAN_BATCH :: 8; // captures per ocr_process call in a scan
|
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
|
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 {
|
ocr_ui: struct {
|
||||||
models: [] Ocr_Model_Info;
|
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.
|
// Owned by the main thread while thread_busy is false; by the background thread while it is true.
|
||||||
engine: *Ocr_Engine;
|
engine: *Ocr_Engine;
|
||||||
engine_error: string; // why the last engine could not be made
|
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: Thread;
|
||||||
thread_started: bool;
|
thread_started: bool;
|
||||||
thread_busy: bool; // set by the main thread, cleared by the background one
|
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_error: string; // why the last single read failed
|
||||||
read_index: s32 = -1;
|
read_index: s32 = -1;
|
||||||
read_seconds: float64;
|
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: Ocr_Scan;
|
||||||
scan_requested: bool; // -scan has started its scan
|
scan_requested: bool; // -scan has started its scan
|
||||||
@ -49,7 +58,7 @@ ocr_ui: struct {
|
|||||||
toast_shown := false;
|
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 {
|
Ocr_Task :: struct {
|
||||||
kind: Ocr_Task_Kind;
|
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).
|
// LABELS_CREATE and LABEL_SET: the label list as it was when the task started (heap).
|
||||||
label_names, label_prompts: [] string;
|
label_names, label_prompts: [] string;
|
||||||
label_hash: 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.
|
// The library scan. The counters and flags are shared with the background thread.
|
||||||
@ -103,7 +116,7 @@ ocr_ui_startup :: () {
|
|||||||
ocr_ui.models = models;
|
ocr_ui.models = models;
|
||||||
if error ocr_ui.models_error = copy_string(error);
|
if error ocr_ui.models_error = copy_string(error);
|
||||||
ocr_ui.model_index = ocr_model_index(app_user_settings.ocr_model);
|
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 :: () {
|
ocr_ui_shutdown :: () {
|
||||||
@ -111,6 +124,7 @@ ocr_ui_shutdown :: () {
|
|||||||
atomic_swap(*ocr_ui.scan.paused, false);
|
atomic_swap(*ocr_ui.scan.paused, false);
|
||||||
if ocr_ui.thread_started thread_is_done(*ocr_ui.thread, -1);
|
if ocr_ui.thread_started thread_is_done(*ocr_ui.thread, -1);
|
||||||
ocr_destroy(ocr_ui.engine);
|
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
|
// 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;
|
if ocr_ui.thread_busy return;
|
||||||
ocr_ui.model_index = index;
|
ocr_ui.model_index = index;
|
||||||
ocr_ui.toast_shown = true; // said once already, or not needed
|
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 {
|
if save {
|
||||||
free(app_user_settings.ocr_model,, context.default_allocator);
|
free(app_user_settings.ocr_model,, context.default_allocator);
|
||||||
app_user_settings.ocr_model = copy_string(ocr_ui.models[index].name,, 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.task.label_hash = copy_string(labels_ui.list_hash);
|
||||||
}
|
}
|
||||||
|
ocr_ui_note_use(task.kind, current_time_monotonic());
|
||||||
ocr_ui.thread_busy = true;
|
ocr_ui.thread_busy = true;
|
||||||
if ocr_ui.thread_started thread_deinit(*ocr_ui.thread);
|
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");
|
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;
|
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
|
// ---------------------------------------------------------------- the background thread
|
||||||
|
|
||||||
ocr_ui_thread_proc :: (thread: *Thread) -> s64 {
|
ocr_ui_thread_proc :: (thread: *Thread) -> s64 {
|
||||||
@ -229,6 +330,11 @@ ocr_ui_thread_proc :: (thread: *Thread) -> s64 {
|
|||||||
case .SEGMENT;
|
case .SEGMENT;
|
||||||
segment_ui_scan_task(); // app/segment_ui.jai
|
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;
|
case .ERASE;
|
||||||
scan := *ocr_ui.scan;
|
scan := *ocr_ui.scan;
|
||||||
// Not in temporary storage, which is reset after each capture.
|
// 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 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 viewer.scan_error { finish(1, "[OCR scan] %", viewer.scan_error); return; }
|
||||||
if !ocr_ui.scan_requested {
|
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.scan_requested = true;
|
||||||
ocr_ui.window_open = true; // its progress bar
|
ocr_ui.window_open = true; // its progress bar
|
||||||
ocr_scan_start();
|
ocr_scan_start();
|
||||||
@ -570,17 +677,18 @@ ocr_scan_ui :: () { using ImGui;
|
|||||||
catalog_ready := viewer.catalog_loaded >= viewer.captures.count;
|
catalog_ready := viewer.catalog_loaded >= viewer.captures.count;
|
||||||
|
|
||||||
if !scan.running {
|
if !scan.running {
|
||||||
can_scan := ocr_ui.engine && !busy && catalog_ready;
|
can_scan := !busy && !ocr_ui.scan_wanted && catalog_ready;
|
||||||
if !can_scan BeginDisabled();
|
if !can_scan BeginDisabled();
|
||||||
label := ifx viewer.search_terms.count then "Scan the captures shown" else "Scan the library";
|
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();
|
if !can_scan EndDisabled();
|
||||||
SameLine();
|
SameLine();
|
||||||
labels_ready := labels_ui.engine && labels_ui.set && labels_ui.set.hash == labels_ui.list_hash;
|
labels_failed := labels_ui.engine_tried && !labels_ui.engine;
|
||||||
if !labels_ready BeginDisabled();
|
if labels_failed BeginDisabled();
|
||||||
Checkbox("Labels too", *labels_ui.scan_labels);
|
Checkbox("Labels too", *labels_ui.scan_labels);
|
||||||
if !labels_ready EndDisabled();
|
if labels_failed 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 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)");
|
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.");
|
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 {
|
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.scan_and_exit ocr_ui_scan_and_exit(busy);
|
||||||
if app.erase_and_exit ocr_ui_erase_and_exit(busy);
|
if app.erase_and_exit ocr_ui_erase_and_exit(busy);
|
||||||
ocr_erase_popup();
|
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 {
|
if !busy && ocr_ui.engine && !ocr_ui.toast_shown {
|
||||||
ocr_ui.toast_shown = true;
|
ocr_ui.toast_shown = true;
|
||||||
in_use, explanation := ocr_gpu_status(ocr_ui.engine);
|
in_use, explanation := ocr_gpu_status(ocr_ui.engine);
|
||||||
@ -685,7 +804,11 @@ ocr_ui_frame :: () { using ImGui;
|
|||||||
} else if ocr_ui.engine_error {
|
} else if ocr_ui.engine_error {
|
||||||
TextColored(app_theme.palette.warning, "OCR could not start:");
|
TextColored(app_theme.palette.warning, "OCR could not start:");
|
||||||
TextWrapped("%", ocr_ui.engine_error);
|
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);
|
in_use, explanation := ocr_gpu_status(ocr_ui.engine);
|
||||||
Text("Running on: %", ifx in_use then "the GPU (CUDA)" else "the CPU");
|
Text("Running on: %", ifx in_use then "the GPU (CUDA)" else "the CPU");
|
||||||
SameLine();
|
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 "";
|
model := ifx ocr_ui.models.count then ocr_ui.models[ocr_ui.model_index].name else "";
|
||||||
has_reading := entry && catalog_section(entry, model) != null;
|
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 !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();
|
if !can_read EndDisabled();
|
||||||
SameLine();
|
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...");
|
TextColored(app_theme.palette.dim, "Reading...");
|
||||||
SameLine();
|
SameLine();
|
||||||
} else if !busy && ocr_ui.read_index == index {
|
} else if !busy && ocr_ui.read_index == index {
|
||||||
@ -789,8 +921,8 @@ ocr_ui_capture_controls :: (index: s32, path: string) { using ImGui;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SameLine();
|
SameLine();
|
||||||
} else if !ocr_ui.engine {
|
} else if ocr_failed {
|
||||||
TextColored(app_theme.palette.dim, "(OCR is not running; see the OCR window)");
|
TextColored(app_theme.palette.dim, "(OCR could not start; see the OCR window)");
|
||||||
SameLine();
|
SameLine();
|
||||||
}
|
}
|
||||||
if SmallButton("OCR window") ocr_ui.window_open = true;
|
if SmallButton("OCR window") ocr_ui.window_open = true;
|
||||||
|
|||||||
@ -112,8 +112,8 @@ screen_capture_frame :: () {
|
|||||||
remove job;
|
remove job;
|
||||||
}
|
}
|
||||||
|
|
||||||
// As an import's are, once the models are free: OCR and labels, then segmentation.
|
// 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.thread_busy && !ocr_ui.scan.running && ocr_ui.engine && labels_ui_settled() {
|
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);
|
ocr_scan_start(screen_capture.scan_pending);
|
||||||
array_reset_keeping_memory(*screen_capture.scan_pending);
|
array_reset_keeping_memory(*screen_capture.scan_pending);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
// 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
|
// 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.
|
// 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_WINDOW :: "Segmentation";
|
||||||
SEGMENT_MODEL :: "mask2former-swin-base-coco-panoptic"; // the one segmentation model; its chunks are keyed by this
|
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.
|
// Owned by the OCR thread while ocr_ui.thread_busy, like ocr_ui.engine.
|
||||||
engine: *Segment_Engine;
|
engine: *Segment_Engine;
|
||||||
engine_error: string;
|
engine_error: string;
|
||||||
|
last_used: Apollo_Time; // main thread: when a scan last used the engine (ocr_ui_unload_idle_models)
|
||||||
|
|
||||||
// Main thread.
|
// Main thread.
|
||||||
window_open: bool;
|
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 viewer.scan_error { finish(1, "[Segment scan] %", viewer.scan_error); return; }
|
||||||
if !segment_ui.scan_requested {
|
if !segment_ui.scan_requested {
|
||||||
// After the catalog, and after the engines made at startup, which use the same thread.
|
// After the catalog.
|
||||||
if busy || viewer.catalog_loaded < viewer.captures.count || !ocr_ui.engine_tried || !labels_ui.engine_tried return;
|
if busy || viewer.catalog_loaded < viewer.captures.count return;
|
||||||
segment_ui.scan_requested = true;
|
segment_ui.scan_requested = true;
|
||||||
segment_ui.window_open = true; // its progress bar
|
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).");
|
if !segment_scan_start(quiet = true) finish(0, "[Segment scan] -segment: nothing to segment (no image in a folder set to segment needs it).");
|
||||||
|
|||||||
@ -908,7 +908,7 @@ viewer_ui :: () { using ImGui;
|
|||||||
engine: *Ocr_Engine;
|
engine: *Ocr_Engine;
|
||||||
if !ocr_ui.thread_busy engine = ocr_ui.engine; // the background thread owns it while busy
|
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");
|
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.
|
// A running OCR scan, as a thin bar under the search, so it shows with the OCR window closed.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user