mcap/src/app/screen_capture.jai
Musa Mahmood 157888d6d8 [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.
2026-10-12 21:58:17 +01:00

244 lines
11 KiB
Plaintext

// Screen capture, as Snagit does it: PrintScreen (the capture_region global hotkey, from any app,
// even with mcap hidden in the tray) freezes every screen, and a rectangle dragged over them is taken
// (app/screen_capture_win32.jai; Windows only so far). What is taken goes onto the clipboard at once,
// and into the library as an .mcap in the folder SCREEN_CAPTURE_FOLDER: the pixels as a PNG, with
// the window it was taken from in metadata.json, its title as WindowName and its program as AppName,
// as Snagit records them, so searches find it the same way. mcap then comes to the front showing
// it in the Capture window, and OCR and labels read it once they are free.
//
// The PNG is made and the file written on a thread of its own, as a large capture takes a second or
// so to compress. A screen recording (app/screen_record.jai) goes into the library through the same
// jobs.
SCREEN_CAPTURE_FOLDER :: "Screen Captures"; // in the library
Screen_Capture_Job :: struct {
shot: Screen_Shot;
date: string; // CaptureDate; heap
// A recording (app/screen_record.jai), not a still; its strings are heap.
video: bool;
recording: string; // the MP4 recorded, deleted once in the library
encoder: string; // "h264_nvenc"
source: string; // how the screen was taken: Record_Stats.source
dropped: s64; // frames the encoder fell too far behind for
paused_seconds: float64; // left out of it
audio: string; // the sound's sources, "Speakers (...); Microphone (...)", or ""
thread: Thread;
busy: bool; // cleared by the thread as it ends
path: string; // what it wrote; heap
error: string; // or why it could not; heap
}
screen_capture: struct {
jobs: [..] *Screen_Capture_Job;
scan_pending: [..] s32; // captures OCR and labels have yet to read
segment_pending: [..] s32; // and then segmentation, when the folder is set to segment
}
// From app_frame when the hotkey fired, outside the ImGui frame: lets a region be chosen, copies it,
// and starts writing it into the library.
screen_capture_take :: () {
if app_exit_shutdown_started() return;
date := import_calendar_text(to_calendar(current_time_consensus(), .LOCAL));
start := current_time_monotonic();
// While recording, the overlay's frozen screen would be in the recording: the screen under the
// cursor is taken at once instead.
shot, ok, error := platform_screen_capture_select(immediate = screen_record.recording);
if !ok {
if error { log_error("[capture] %.", error); toast_show(.Error, "Screen capture: %", error); }
else log("[capture] Cancelled.");
return;
}
log("[capture] % x % at %, % (%) from \"%\" (%), chosen in % s.", shot.width, shot.height, shot.left, shot.top,
ifx shot.whole_screen then "the whole screen" else "a region", shot.window_title, shot.app_name,
FF(to_float64_seconds(current_time_monotonic() - start), 1));
if !platform_copy_image_to_clipboard(shot.pixels, shot.width, shot.height) log_error("[capture] Could not put the capture on the clipboard.");
job := New(Screen_Capture_Job,, context.default_allocator);
job.shot = shot;
job.date = copy_string(date,, context.default_allocator);
screen_capture_job_start(job);
}
// Writes the job into the library on a thread of its own; screen_capture_frame takes it from there.
// The job is freed when it cannot start.
screen_capture_job_start :: (job: *Screen_Capture_Job) -> bool {
job.busy = true;
if !thread_init(*job.thread, screen_capture_save_proc, temporary_storage_size = 1024 * 1024, name = "screen capture") {
what := ifx job.video then "recording" else "capture";
log_error("[capture] Could not start the thread to save the %.", what);
toast_show(.Error, "The screen % could not be saved", what);
screen_capture_job_free(job);
return false;
}
thread_start(*job.thread, job);
screen_capture.jobs.allocator = context.default_allocator;
array_add(*screen_capture.jobs, job);
return true;
}
// Every frame, even while hidden in the tray: the captures written, into the grid and the Capture
// window, and then read.
screen_capture_frame :: () {
if app_exit_shutdown_started() return;
screen_record_frame();
for job: screen_capture.jobs {
if atomic_read(*job.busy) continue;
thread_is_done(*job.thread, -1);
thread_deinit(*job.thread);
what := ifx job.video then "recording" else "capture";
if job.video screen_record_saved(job.path, job.error);
if job.error {
log_error("[capture] Could not save the %: %", what, job.error);
toast_show(.Error, "The screen % could not be saved: %", what, job.error);
} else {
log("[capture] Saved %.", job.path);
index := viewer_add_capture(job.path);
if index >= 0 {
viewer_request_full(index);
screen_capture.scan_pending.allocator = context.default_allocator;
screen_capture.segment_pending.allocator = context.default_allocator;
array_add(*screen_capture.scan_pending, index);
if !job.video && library_folder_segments(SCREEN_CAPTURE_FOLDER) array_add(*screen_capture.segment_pending, index);
}
app.restore_window_this_frame = true;
if job.video toast_show(.Success, "% x % recorded", job.shot.width, job.shot.height);
else toast_show(.Success, "% x % captured and copied to the clipboard", job.shot.width, job.shot.height);
}
screen_capture_job_free(job);
remove job;
}
// 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);
}
// Once their catalog entries are in: the scan takes only images, and they say which.
if screen_capture.segment_pending.count && !screen_capture.scan_pending.count && !ocr_ui.thread_busy && !ocr_ui.scan.running {
catalogued := true;
for screen_capture.segment_pending if !viewer.captures[it].catalog && !viewer.captures[it].removed { catalogued = false; break; }
if catalogued {
segment_scan_start(screen_capture.segment_pending, quiet = true);
array_reset_keeping_memory(*screen_capture.segment_pending);
}
}
}
// On the shutdown thread: a capture being written is finished, not lost.
screen_capture_shutdown :: () {
screen_record_shutdown(); // a recording running is stopped and becomes a job, finished below
for screen_capture.jobs thread_is_done(*it.thread, -1);
}
screen_capture_job_free :: (job: *Screen_Capture_Job) {
push_allocator(context.default_allocator);
screen_shot_free(*job.shot);
free(job.date);
free(job.path);
free(job.error);
free(job.recording);
free(job.encoder);
free(job.source);
free(job.audio);
free(job);
}
screen_capture_save_proc :: (thread: *Thread) -> s64 {
job := cast(*Screen_Capture_Job) thread.data;
context.allocator = context.default_allocator;
defer atomic_swap(*job.busy, false);
path, error: string;
if job.video path, error = screen_record_write(job);
else path, error = screen_capture_write(job.shot, job.date);
if error job.error = copy_string(error);
else job.path = copy_string(path);
return 0;
}
// Writes the capture into the library: its path, or why it could not be (temp).
screen_capture_write :: (shot: Screen_Shot, date: string) -> path: string, error: string {
image := Image.{pixels = shot.pixels.data, width = shot.width, height = shot.height};
png, _, _, png_ok, png_error := image_preview_png(image, max(shot.width, shot.height)); // at its own size
if !png_ok return "", png_error;
preview, preview_width, preview_height, preview_ok, preview_error := image_preview_png(image, MCAP_PREVIEW_MAX_SIDE);
if !preview_ok return "", preview_error;
hash := import_hash(png);
doc := New(Capture_Document,, temp);
capture_document_init(doc, temp);
doc.comment = tprint("Captured by % %.", APP_NAME, VERSION);
metadata := capture_add_file(doc, "metadata.json");
import_add(doc, metadata, "AppName", import_text(shot.app_name));
import_add(doc, metadata, "CaptureDate", date);
import_add(doc, metadata, "WindowName", import_text(shot.window_title));
info := capture_add_file(doc, "capture.json");
import_add(doc, info, "Region", ifx shot.whole_screen then "screen" else "selection");
import_add_number(doc, info, "Left", xx shot.left);
import_add_number(doc, info, "Top", xx shot.top);
import_add_number(doc, info, "Width", xx shot.width);
import_add_number(doc, info, "Height", xx shot.height);
if shot.app_path import_add(doc, info, "AppPath", import_text(shot.app_path));
import_add(doc, info, "ContentHash", import_hash_text(hash));
import_add(doc, info, "Format", "PNG");
name := import_out_name(date, hash);
text, text_ok := import_document_text(doc, name);
if !text_ok return "", "its Capture Text does not read back cleanly (see the Event Log)";
m: Mcap;
m.text = text;
m.preview = .{width = cast(u16) preview_width, height = cast(u16) preview_height, png = preview};
m.original_name = tprint("%.png", path_strip_extension(name));
m.original = png;
m.rendered_kind = .NONE;
serializer, serialized, serialize_error := mcap_serialize(m,, temp);
if !serialized return "", serialize_error;
path, folder_error := screen_capture_out_path(name);
if folder_error return "", folder_error;
written, write_error := mcap_write(path, cast([] u8) string.{serializer.count, serializer.data});
if !written return "", write_error;
return path, "";
}
// Where a capture named name goes in SCREEN_CAPTURE_FOLDER, made if need be: name, or name_2 ... when
// another file has it. Temp.
screen_capture_out_path :: (name: string) -> path: string, error: string {
folder := tprint("%/%", viewer_folder(), SCREEN_CAPTURE_FOLDER);
if !make_directory_if_it_does_not_exist(folder, recursive = true) return "", tprint("% could not be made", folder);
path := tprint("%/%", folder, name);
number := 2;
while file_exists(path) {
path = tprint("%/%_%.%", folder, path_strip_extension(name), number, MCAP_EXTENSION);
number += 1;
}
return path, "";
}
#if OS != .WINDOWS {
Screen_Shot :: struct {
pixels: [] u8;
width, height: s32;
left, top: s32;
whole_screen: bool;
window_title, app_name, app_path: string;
}
screen_shot_free :: (shot: *Screen_Shot) {
array_free(shot.pixels,, context.default_allocator);
shot.* = .{};
}
platform_screen_capture_select :: (video := false, immediate := false) -> Screen_Shot, ok: bool, error: string {
return .{}, false, "not available on this system yet";
}
platform_copy_image_to_clipboard :: (pixels: [] u8, width: s32, height: s32) -> bool {
return false;
}
}