- 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.
244 lines
11 KiB
Plaintext
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;
|
|
}
|
|
}
|