mcap/src/base/platform/platform_windows.jai
Musa Mahmood be9ad707c4 [0.1.17] mcap.exe FILE opens a file, beside a running mcap in view mode; a Command Line window; thumbnails no longer stuck on "Cannot read"
mcap.exe FILE, with FILE first (as Explorer's Open with passes it), opens an .mcap, image, video or
.snagx (app/open_file.jai). With no mcap running, the app starts as usual: an .mcap in the library
opens in the Capture window, anything else in the Opened File window, which says where the library
has it already (by its hash in .import-index) or imports it into the library folder typed or chosen.
With mcap running, a second window opens in view mode: the file alone, with Import, and Open in mcap
/ Show in mcap, which hand it to the running copy (a new Open_File message) and close the view. The
library is not loaded or watched, no model loads, and it has no tray icon, no global hotkeys, and
writes no settings or layout.

Import takes a single file as its source too (and -import FILE DEST); an .mcap is copied in as it is,
while a folder's import still skips them. -open-import DEST imports the opened file as its Import
button does, then exits.

-h and --help open a Command Line window listing the options, also toggled from the Debug Panel; with
mcap running, that one comes forward with it (a Show_Help message).

Thumbnails: the "First preview shown" log line, put between creating a preview's texture and its
else, had every preview after the first marked failed. Once its texture was released (scrolled far
away, or the switch from WARP to the GPU at startup) it was never decoded again and showed "Cannot
read".
2026-10-14 08:00:02 +01:00

874 lines
34 KiB
Plaintext

platform_load_platform_information :: () {
// load_platform_information_shared();
}
platform_get_path_program_data :: inline () -> string {
// #NOTE: This is slow due to loading shell32 - see comment attached to get_path_os_program_data();
return copy_string(get_path_os_program_data());
}
platform_get_path_config_data :: inline () -> string {
return platform_get_path_program_data();
}
Platform :: struct { // Windows
registered_global_hotkey_ids: [GLOBAL_HOTKEY_ACTION_COUNT * MAX_HOTKEY_BINDINGS] s32;
registered_global_hotkey_count: s64;
global_hotkeys_failed: string; // the combinations that could not be registered, for the main window; heap
tray_icon_configured: bool; // nid and its icon are ready; adding it can still fail while Explorer is not
tray_icon_failed: bool; // minimizing then goes to the taskbar; the main window says so
hwnd: HWND;
// WM_PAINT draws frames with this context while the modal size/move loop blocks the main loop.
main_context: *#Context;
pumping_messages: bool;
in_size_move: bool;
in_live_resize_frame: bool;
live_resize_frame_drawn: bool; // during this pump; see Windows_Read_Messages
swallow_hotkey_character: bool; // see win32_key_down_is_bound
resetting_window_placement: bool;
pending_dpi_scale: float32; // from WM_DPICHANGED, applied by win32_apply_dpi_change
nid: NOTIFYICONDATAW;
icon_menu: HMENU;
tray_icon: HICON;
taskbar_created_message: u32; // broadcast when Explorer restarts, which drops every tray icon
all_cpus: []Windows_CPUID;
performance_cpus: []Windows_CPUID;
efficiency_cpus: []Windows_CPUID;
}
windows_open_directory :: (path: string) -> bool {
push_allocator(temp);
quoted_path := windows_quote_argument(path);
result := cast(s64)ShellExecuteW(null, utf8_to_wide("open"), utf8_to_wide("explorer.exe"),
utf8_to_wide(quoted_path), null, SW_SHOWNORMAL);
if (result <= 32) {
log_error("[windows_open_directory] ShellExecuteW failed with args `open explorer.exe %`", quoted_path);
os_log_error();
return false;
}
return true;
}
windows_open_file_in_default_program :: (path: string, working_directory: string, args := "") -> bool {
if is_directory(path) {
return windows_open_directory(path);
}
push_allocator(temp);
path_copy := copy_string(path,, temp);
path_overwrite_separators(path_copy, #char "\\");
working_dir_copy := copy_string(working_directory,, temp);
path_overwrite_separators(working_dir_copy, #char "\\");
// We use ShellExecuteExW so we can properly detach from new launched process:
full_path_w := utf8_to_wide(path_copy);
working_dir_w := utf8_to_wide(working_dir_copy);
SEE_MASK_NOCLOSEPROCESS :: 0x00000040;
sei: SHELLEXECUTEINFOW;
defer CloseHandle(sei.hProcess);
sei.cbSize = size_of(SHELLEXECUTEINFOW);
sei.fMask = SEE_MASK_NOCLOSEPROCESS; // | SEE_MASK_NO_CONSOLE
sei.lpFile = full_path_w;
sei.lpDirectory = working_dir_w;
sei.nShow = SW_SHOWNORMAL;
if args.count sei.lpParameters = utf8_to_wide(args);
result := ShellExecuteExW(*sei);
if !result {
log_error("[windows_open_file_in_default_program] ShellExecuteExW failed with args % % %",
path, working_directory, args);
os_log_error();
}
return cast(bool)result;
}
// Quote one argument for a Windows command line, by the CommandLineToArgvW rules that
// ShellExecuteW's parameter string is parsed with: a run of backslashes is literal except before a
// quote, where it escapes. The backslash rule is the one that matters for paths -- without it
// "D:\TempLocal\" has its closing quote escaped by the trailing separator.
windows_quote_argument :: (s: string) -> string {
sb: String_Builder;
append(*sb, #char "\"");
backslashes := 0;
for i: 0..s.count-1 {
c := s[i];
if c == #char "\\" {
backslashes += 1;
continue;
}
run := ifx c == #char "\"" then backslashes * 2 + 1 else backslashes;
for 1..run append(*sb, #char "\\");
backslashes = 0;
append(*sb, c);
}
for 1..backslashes * 2 append(*sb, #char "\\");
append(*sb, #char "\"");
return builder_to_string(*sb);
}
platform_open_file_in_default_program :: (path: string, working_directory: string) {
windows_open_file_in_default_program(path, working_directory);
}
platform_open_directory :: windows_open_directory;
// Called every time the application receives a message
WndProc :: (hWnd: HWND, message: u32, wParam: WPARAM, lParam: LPARAM) -> LRESULT #c_call {
using platform;
TEMPORARY_STORAGE_SIZE :: 1000;
storage_buf: [TEMPORARY_STORAGE_SIZE] u8;
storage: Temporary_Storage;
storage.data = storage_buf.data;
storage.size = TEMPORARY_STORAGE_SIZE;
new_context: #Context;
new_context.temporary_storage = *storage;
use_app_logger(*new_context);
push_context,defer_pop new_context;
defer reset_temporary_storage(); // frees any overflow pages; the buffer itself is on the stack
push_allocator(temp);
if message == {
case WM_KEYDOWN; #through;
case WM_SYSKEYDOWN;
swallow_hotkey_character = win32_key_down_is_bound(reset_window_hotkey, wParam);
case WM_KEYUP; #through;
case WM_SYSKEYUP;
swallow_hotkey_character = false;
case WM_CHAR; #through;
case WM_SYSCHAR;
if swallow_hotkey_character return 0;
case WM_DPICHANGED;
// The X and Y DPI are always equal for a window.
if hWnd == hwnd pending_dpi_scale = cast(float32) (wParam & 0xFFFF) / 96.0;
// ImGui would apply the suggested rect, the old size scaled by the DPI ratio, over the
// reset's rect, which is already in the new monitor's pixels.
if resetting_window_placement return 0;
}
if (ImGui.ImplWin32_WndProcHandler(hwnd, message, wParam, lParam)) {
return cast(LRESULT) true;
}
if swallow_hotkey_character && (message == WM_KEYDOWN || message == WM_SYSKEYDOWN) return 0;
if taskbar_created_message && message == taskbar_created_message {
// Without the icon, a window hidden in the tray could only come back by hotkey.
if app.minimized_to_tray && !Win32_Show_Tray_Icon() {
ShowWindow(hwnd, SW_MINIMIZE);
app.minimized_to_tray = false;
}
return 0;
}
if message == {
case WM_SYSCOMMAND;
SC_MINIMIZE :: 0xF020; SC_CLOSE :: 0xF060;
// View mode (app/open_file.jai) has no tray: close ends it, and minimize is the system's own.
if app.view_mode && (wParam & 0xFFF0) == SC_CLOSE { app.should_exit = true; return 0; }
if app.view_mode && (wParam & 0xFFF0) == SC_MINIMIZE return DefWindowProcW(hWnd, message, wParam, lParam);
if (wParam & 0xFFF0) == {
case SC_CLOSE; #through;
case SC_MINIMIZE;
app.minimize_to_tray_this_frame = true;
case SC_KEYMENU;
// A tapped Alt would otherwise enter the system menu's modal loop, which stops frames.
case;
return DefWindowProcW(hWnd, message, wParam, lParam);
}
return 0;
case WM_SIZE;
if (wParam == SIZE_MINIMIZED) {
return 0;
}
g_resizeWidth = cast(u32)LOWORD(lParam);
g_resizeHeight = cast(u32)HIWORD(lParam);
return 0;
case WM_ENTERSIZEMOVE;
in_size_move = true;
SetTimer(hWnd, LIVE_RESIZE_TIMER_ID, LIVE_RESIZE_TIMER_MS, null);
return 0;
case WM_EXITSIZEMOVE;
in_size_move = false;
KillTimer(hWnd, LIVE_RESIZE_TIMER_ID);
return 0;
case WM_TIMER;
if wParam != LIVE_RESIZE_TIMER_ID return DefWindowProcW(hWnd, message, wParam, lParam);
if in_size_move InvalidateRect(hWnd, null, .FALSE);
return 0;
case WM_PAINT;
if !in_size_move || !pumping_messages || in_live_resize_frame || !main_context {
return DefWindowProcW(hWnd, message, wParam, lParam);
}
ValidateRect(hWnd, null);
in_live_resize_frame = true;
live_resize_frame_drawn = true;
push_context main_context.* {
auto_release_temp();
app_frame(in_live_resize = true);
}
in_live_resize_frame = false;
return 0;
case WM_CREATE;
icon_menu = CreatePopupMenu();
AppendMenuW(icon_menu, MF_STRING, xx THIS_ICON_CONTEXT_MENU_ITEM_ID, utf8_to_wide("Exit"));
return 0;
case WM_DESTROY;
PostQuitMessage(0);
return 0;
// Registered to the window rather than the thread: a modal loop (a live resize, the tray menu,
// a message box) dispatches window messages but discards the thread's own.
case WM_HOTKEY;
global_hotkey_fired(cast(s64) wParam);
return 0;
// wParam is true when the session really ends; Windows may end the app as soon as this returns.
case WM_ENDSESSION;
if wParam {
app_exit_shutdown_run_now();
logger_flush(2000); // close_logger may never run
}
return 0;
case WM_TRAYICON;
// Most applications perform actions on button up (after click), but we do
// on button down to make it feel as responsive as possible.
if lParam == WM_LBUTTONDOWN {
#if DEBUG { log("Restoring application"); }
app.restore_window_this_frame = true;
}
if lParam == WM_RBUTTONDOWN {
curPos: POINT;
GetCursorPos(*curPos);
// SetForegroundWindow so the popup shows on top
SetForegroundWindow(hwnd);
TPM_NONOTIFY :: 0x0080;
TPM_RETURNCMD :: 0x0100;
clicked := TrackPopupMenu(icon_menu, TPM_RETURNCMD | TPM_NONOTIFY, curPos.x, curPos.y, 0, hwnd, null);
if clicked == THIS_ICON_CONTEXT_MENU_ITEM_ID {
#if DEBUG { log("Posting Quit Message."); }
PostQuitMessage(0);
}
}
return 0;
case;
return DefWindowProcW(hWnd, message, wParam, lParam);
}
return 0;
}
// =-----------PROCEDURES-------------------------------------------------------= //
// Registers the [global_hotkeys] bindings, replacing whatever was registered before. Each binding
// is registered on its own: Alt+Space is PowerToys Run's default, and losing it must not cost the
// others.
platform_apply_global_hotkeys :: (actions: [] Hotkey_Action) { using platform;
platform_unregister_global_hotkeys();
failed: [..] string;
failed.allocator = temp;
for action, action_index: actions {
for combo, binding_index: array_view(action.hotkey.bindings, 0, action.hotkey.binding_count) {
vk := win32_virtual_key(combo.key);
if !vk {
log_error("[hotkeys] % cannot be a global hotkey (%); its key is not supported.", to_string(combo), action.name);
array_add(*failed, to_string(combo));
continue;
}
modifiers: u32 = MOD_NOREPEAT;
if combo.ctrl modifiers |= MOD_CONTROL;
if combo.shift modifiers |= MOD_SHIFT;
if combo.alt modifiers |= MOD_ALT;
id := global_hotkey_id(action_index, binding_index);
if !RegisterHotKey(hwnd, id, modifiers, vk) {
log_error("[hotkeys] Could not register the global hotkey % (%); another program may be using it.", to_string(combo), action.name);
if vk == VK_SNAPSHOT log_error("[hotkeys] PrintScreen is taken by Snagit when it runs, and by Windows when Settings > Accessibility > Keyboard > \"Use the Print screen key to open screen capture\" is on.");
array_add(*failed, to_string(combo));
continue;
}
registered_global_hotkey_ids[registered_global_hotkey_count] = id;
registered_global_hotkey_count += 1;
log("[hotkeys] Global hotkey % registered (%).", to_string(combo), action.name);
}
}
free(global_hotkeys_failed,, context.default_allocator);
global_hotkeys_failed = join(..failed, " and ",, context.default_allocator);
}
platform_unregister_global_hotkeys :: () { using platform;
for 0..registered_global_hotkey_count - 1 UnregisterHotKey(hwnd, registered_global_hotkey_ids[it]);
registered_global_hotkey_count = 0;
}
// The keys a global hotkey may use; 0 for any other.
win32_virtual_key :: (key: ImGui.Key) -> u32 {
k := cast(s64) key;
if k >= cast(s64) ImGui.Key.A && k <= cast(s64) ImGui.Key.Z return cast(u32) (#char "A" + k - cast(s64) ImGui.Key.A);
if k >= cast(s64) ImGui.Key._0 && k <= cast(s64) ImGui.Key._9 return cast(u32) (#char "0" + k - cast(s64) ImGui.Key._0);
if k >= cast(s64) ImGui.Key.F1 && k <= cast(s64) ImGui.Key.F24 return cast(u32) (VK_F1 + k - cast(s64) ImGui.Key.F1);
if key == {
case .PrintScreen; return VK_SNAPSHOT;
case .Space; return VK_SPACE;
case .Enter; return VK_RETURN;
case .Tab; return VK_TAB;
case .Escape; return VK_ESCAPE;
case .LeftArrow; return VK_LEFT;
case .RightArrow; return VK_RIGHT;
case .UpArrow; return VK_UP;
case .DownArrow; return VK_DOWN;
case .PageUp; return VK_PRIOR;
case .PageDown; return VK_NEXT;
case .Home; return VK_HOME;
case .End; return VK_END;
case .Insert; return VK_INSERT;
case .Delete; return VK_DELETE;
}
return 0;
}
// Also fails while Explorer is not running, so it is checked on every call.
Win32_Show_Tray_Icon :: () -> bool { using platform;
if !tray_icon_configured return false;
if !cast(bool) Shell_NotifyIconW(NIM_ADD, *nid) {
log_error("Shell_NotifyIconW(NIM_ADD) failed; minimizing to the taskbar instead of the tray.");
os_log_error();
tray_icon_failed = true;
return false;
}
tray_icon_failed = false; // an earlier failure while Explorer was down has passed
return true;
}
Win32_Hide_Tray_Icon :: () { using platform;
if tray_icon_configured Shell_NotifyIconW(NIM_DELETE, *nid);
}
Win32_Set_Tray_Icon :: () { using platform;
taskbar_created_message = RegisterWindowMessageW(utf8_to_wide("TaskbarCreated",, temp));
NIF_MESSAGE :: 0x0001; NIF_ICON :: 0x0002; NIF_TIP :: 0x0004;
// Tooltip text must be less than 64 chars for some reason...
Tooltip_Text :: APP_TRAY_TOOLTIP_TEXT;
Tooltip_Text_u16 := utf8_to_wide(Tooltip_Text,, temp);
nid.cbSize = size_of(NOTIFYICONDATAW);
nid.hWnd = hwnd;
nid.uID = THIS_ICON_ID; // ID of the icon
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nid.uCallbackMessage = WM_TRAYICON; // Custom message when interacting with the tray icon
tray_icon = LoadImageW(null, utf8_to_wide(get_app_tray_icon_path(),, temp), IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
if tray_icon == null {
log_error("LoadImageW failed for the tray icon; minimizing to the taskbar instead of the tray.");
os_log_error();
tray_icon_failed = true;
return;
}
nid.hIcon = tray_icon;
memcpy(nid.szTip.data, Tooltip_Text_u16, Tooltip_Text.count * size_of(u16));
// Configured even if this first add fails: at autostart Explorer's notification area may not be
// ready yet, and Win32_Show_Tray_Icon adds the icon again on every minimize.
tray_icon_configured = true;
if !cast(bool) Shell_NotifyIconW(NIM_ADD, *nid) {
log_error("Shell_NotifyIconW(NIM_ADD) failed; minimizing to the taskbar until it succeeds.");
os_log_error();
tray_icon_failed = true;
}
}
// ImGui rescales the fonts per monitor by itself, but the theme's sizes are baked at one scale, so
// they are baked again for the monitor the main window is now on. Must run outside a frame.
win32_apply_dpi_change :: () { using platform;
scale := pending_dpi_scale;
pending_dpi_scale = 0;
if scale <= 0 || scale == app.win_scale return;
log("[dpi] The window is now on a monitor with DPI scale % (was %); rescaling the layout.", scale, app.win_scale);
app.win_scale = scale;
app_theme_bake(scale);
}
// Returns as soon as a message is queued (a hotkey, a tray click, the window being uncovered), or
// after timeout_ms.
platform_wait_while_hidden :: (timeout_ms: u32) {
MsgWaitForMultipleObjectsEx(0, null, timeout_ms, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
}
platform_minimize_to_tray :: Win32_Minimize_Window_To_Tray;
Win32_Minimize_Window_To_Tray :: () {
using platform;
// Minimize normally if we're not able to show the tray icon.
if !Win32_Show_Tray_Icon() {
ShowWindow(hwnd, SW_MINIMIZE);
return;
}
ShowWindow(hwnd, SW_HIDE);
app.minimized_to_tray = true;
}
// Win32_Bring_Window_To_Foreground's first step; does nothing unless the window is in the tray.
Win32_Restore_Window_From_Tray :: () {
using platform;
if app.minimized_to_tray {
ShowWindow(hwnd, SW_SHOW);
app.minimized_to_tray = false;
Win32_Hide_Tray_Icon();
}
}
platform_restore_window :: Win32_Bring_Window_To_Foreground;
Win32_Bring_Window_To_Foreground :: () {
using platform;
#if DEBUG { log("Attempting to bring window to foreground"); }
Win32_Restore_Window_From_Tray();
if Win32_Window_Is_Minimized() {
ShowWindow(hwnd, SW_RESTORE);
}
SetForegroundWindow(hwnd);
}
Win32_Window_Is_Minimized :: () -> bool {
return cast(bool) IsIconic(platform.hwnd);
}
// Returns false when a live-resize frame drew during the pump. That frame rebuilt the per-frame
// state (anything app_ui_frame_start builds) in temporary storage it then released, so the calling
// frame must not go on to draw with what its own #frameStart built.
Windows_Read_Messages :: () -> frame_state_valid: bool {
platform.pumping_messages = true;
platform.live_resize_frame_drawn = false;
defer platform.pumping_messages = false;
msg: MSG;
while (PeekMessageW(*msg, null, 0, 0, PM_REMOVE)) {
TranslateMessage(*msg);
DispatchMessageW(*msg);
if (msg.message == WM_QUIT) {
app.should_exit = true;
}
}
return !platform.live_resize_frame_drawn;
}
windows_enable_large_pages_if_possible :: () {
token: HANDLE = ---;
if OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, *token) {
luid: u64;
if LookupPrivilegeValue(null, SE_LOCK_MEMORY_NAME, *luid) {
priv : TOKEN_PRIVILEGES;
priv.PrivilegeCount = 1;
priv.Privileges[0].Luid = luid;
priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
// A nonzero return only means the call itself worked, not that the privilege was
// granted: an account without SeLockMemoryPrivilege succeeds here and reports
// ERROR_NOT_ALL_ASSIGNED. AdjustTokenPrivileges sets the last error to ERROR_SUCCESS
// when it assigned everything, so the error code has to be read as well or this flag
// claims a privilege the process does not hold.
adjusted := AdjustTokenPrivileges(token, 0, *priv, size_of(TOKEN_PRIVILEGES), null, null) != 0;
error_code := GetLastError();
app.large_pages_allowed = adjusted && error_code == 0;
// The code is logged from the captured value rather than through os_log_error, because
// logging itself can reset the thread's last error before it could be read again.
if !adjusted {
log_error("[windows_enable_large_pages_if_possible] AdjustTokenPrivileges failed with code %: %",
error_code, get_error_string(error_code));
} else if error_code == ERROR_NOT_ALL_ASSIGNED {
// Granted under secpol.msc > Local Policies > User Rights Assignment > Lock pages in
// memory, and it only takes effect after a new logon. Not having it is not an error:
// arenas asking for large pages fall back to 4 KB ones.
// NOTE: (IMPORTANT) Lock pages in memory (SeLockMemoryPrivilege) is assigned to
// accounts/groups, not automatically to everyone who happens to have administrator privileges.
log("[windows_enable_large_pages_if_possible] SeLockMemoryPrivilege is not held by this account; large pages are unavailable.");
} else if error_code != 0 {
log_error("[windows_enable_large_pages_if_possible] AdjustTokenPrivileges reported an unexpected code %: %",
error_code, get_error_string(error_code));
}
log("app.large_pages_allowed = %", app.large_pages_allowed);
} else {
log_error("[windows_enable_large_pages_if_possible] LookupPrivilegeValue failed.");
}
CloseHandle(token);
} else {
log_error("[windows_enable_large_pages_if_possible] OpenProcessToken failed.");
}
}
platform_set_thread_affinity_all :: #bake_arguments platform_set_thread_affinity(affinity=.All);
platform_set_thread_affinity :: (thread: *Thread, affinity: CPUID_Type) {
using platform;
cpus: []Windows_CPUID;
if #complete affinity == {
case .All; cpus = all_cpus;
case .Performance; cpus = performance_cpus;
case .Efficiency; cpus = efficiency_cpus;
// case .Top_Performance; cpus = top_cpus;
}
windows_set_thread_affinity(thread.windows_thread, cpus);
}
windows_set_thread_affinity :: (thread: HANDLE, cpus: []Windows_CPUID) {
if !cpus.count || thread == INVALID_HANDLE_VALUE return;
success := SetThreadSelectedCpuSets(thread, cpus.data, xx cpus.count);
if !success {
log_error("SetThreadSelectedCpuSets failed on thread %!", thread);
}
}
windows_get_cpu_information :: () {
push_allocator(context.default_allocator);
using platform;
all_cpus = array_copy(windows_get_cpu_ids(.All,, temp));
performance_cpus = array_copy(windows_get_cpu_ids(.Performance,, temp));
efficiency_cpus = array_copy(windows_get_cpu_ids(.Efficiency,, temp));
// top_cpus = array_copy(windows_get_cpu_ids(.Top_Performance,, temp));
}
Windows_CPUID :: #type,isa u32;
windows_get_cpu_ids :: ($type: CPUID_Type) -> []Windows_CPUID {
length: DWORD;
GetSystemCpuSetInformation(null, 0, *length, GetCurrentProcess(), 0);
buffer := NewArray(length, u8,, temp);
GetSystemCpuSetInformation(xx buffer.data, length, *length, GetCurrentProcess(), 0);
item_count := length / size_of(SYSTEM_CPU_SET_INFORMATION);
info_array: []SYSTEM_CPU_SET_INFORMATION = .{item_count, cast(*SYSTEM_CPU_SET_INFORMATION)buffer.data};
core_ids: [..]Windows_CPUID;
for 0..info_array.count-1 {
// EfficiencyClass == 0 usually means E-Core, and 1 means P-core, 2 means best P-cores (usually 2 CPUs have this value)
#if type == .All { type_matches := info_array[it].CpuSet.EfficiencyClass >= 0; }
#if type == .Performance { type_matches := info_array[it].CpuSet.EfficiencyClass >= 1; }
#if type == .Efficiency { type_matches := info_array[it].CpuSet.EfficiencyClass == 0; }
// #if type == .Top_Performance { type_matches := info_array[it].CpuSet.EfficiencyClass == 2; }
if info_array[it].Type == .CpuSetInformation && type_matches {
cpu := info_array[it].CpuSet;
array_add(*core_ids, xx cpu.Id);
}
}
return core_ids;
}
platform_update_memory_usage_metrics :: () { using perf.memory_usage;
pmc: PROCESS_MEMORY_COUNTERS;
if (GetProcessMemoryInfo(GetCurrentProcess(), *pmc, size_of(PROCESS_MEMORY_COUNTERS))) {
working_set = xx pmc.WorkingSetSize;
virtual_memory = xx pmc.PagefileUsage;
peak_working_set = xx pmc.PeakWorkingSetSize;
peak_virtual_memory = xx pmc.PeakPagefileUsage;
}
}
windows_check_if_we_are_running_as_administrator :: () {
token: HANDLE;
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, *token) {
elevation: TOKEN_ELEVATION = {};
size: DWORD = size_of(TOKEN_ELEVATION);
ok := GetTokenInformation(token, .TokenElevation, *elevation, size_of(TOKEN_ELEVATION), *size);
CloseHandle(token);
app.is_admin = ok && elevation.TokenIsElevated;
}
}
// %NAME% expanded as cmd does. The path is returned as is when it has none or expansion fails. Temp.
windows_expand_environment :: (path: string) -> string {
if !contains(path, "%") return path;
ExpandEnvironmentStringsW :: (lpSrc: LPCWSTR, lpDst: LPWSTR, nSize: DWORD) -> DWORD #foreign kernel32;
buffer: [4096] u16;
count := ExpandEnvironmentStringsW(utf8_to_wide(path,, temp), buffer.data, buffer.count);
if count == 0 || count > buffer.count return path;
expanded, ok := wide_to_utf8(buffer.data,, temp);
return ifx ok then expanded else path;
}
// A BCP 47 tag such as ja-JP, or "". Temp.
platform_preferred_language :: () -> string {
buffer: [LOCALE_NAME_MAX_LENGTH] u16;
if GetUserDefaultLocaleName(buffer.data, buffer.count) <= 1 return "";
name, ok := wide_to_utf8(buffer.data,, temp);
return ifx ok then name else "";
}
// Temp.
platform_cjk_font_candidates :: () -> [] Cjk_Font {
fonts := windows_expand_environment("%WINDIR%\\Fonts");
list: [..] Cjk_Font;
list.allocator = temp;
cjk_candidate(*list, .SIMPLIFIED, tprint("%\\msyh.ttc", fonts), "Microsoft YaHei");
cjk_candidate(*list, .TRADITIONAL, tprint("%\\msjh.ttc", fonts), "Microsoft JhengHei");
cjk_candidate(*list, .JAPANESE, tprint("%\\YuGothM.ttc", fonts), "Yu Gothic");
cjk_candidate(*list, .JAPANESE, tprint("%\\meiryo.ttc", fonts), "Meiryo");
cjk_candidate(*list, .KOREAN, tprint("%\\malgun.ttf", fonts), "Malgun Gothic");
return list;
}
// Read-only for the life of the process; pages are read from the file only as they are touched.
platform_map_file_read_only :: (path: string) -> (data: string, ok: bool) {
file, opened := file_open(path);
if !opened return "", false;
defer file_close(*file);
size, size_ok := file_length(file);
if !size_ok || size <= 0 return "", false;
mapping := CreateFileMappingW(file.handle, null, PAGE_READONLY, 0, 0, null);
if !mapping return "", false;
defer CloseHandle(mapping); // the view keeps the mapping alive
view := MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0);
if !view return "", false;
return string.{size, cast(*u8) view}, true;
}
// Drops what the system's file cache holds of the file, so the next read comes from the disk (-cold).
// Opening a file unbuffered makes the cache manager flush and purge its pages, when nothing has the
// file mapped. False when it cannot be opened.
platform_evict_file_cache :: (path: string) -> bool {
FILE_FLAG_NO_BUFFERING :: 0x20000000;
wide := utf8_to_wide(path,, temp);
handle := CreateFileW(wide, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, null, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING);
if handle == INVALID_HANDLE_VALUE return false;
CloseHandle(handle);
return true;
}
// "" when the variable is unset or empty. In the current allocator.
windows_environment_variable :: (name: string) -> string {
name_w := utf8_to_wide(name,, temp);
length := GetEnvironmentVariableW(name_w, null, 0);
if !length return "";
buffer := NewArray(length, u16,, temp);
written := GetEnvironmentVariableW(name_w, buffer.data, length);
if !written || written >= length return "";
value, ok := wide_to_utf8(buffer.data, cast(s32) written);
return ifx ok then value else "";
}
platform_get_centered_window_dimensions :: (open_on_biggest: bool) -> s32, s32, s32, s32 {
if !monitors_enumerated {
enumerate_monitors();
monitors_enumerated = true;
}
assert(monitors.count > 0, "Unexpectedly no monitors found. This is a bug.");
monitor := monitors[0];
if open_on_biggest {
max_area := 0;
for monitors {
width := it.right - it.left;
height := it.bottom - it.top;
area := width * height;
if max_area < area {
monitor = it;
max_area = area;
} else if max_area == area && it.primary {
monitor = it; // prefer primary if there's no biggest monitor
}
}
} else {
for monitors {
if it.primary {
monitor = it;
}
}
}
monitor_width := monitor.right - monitor.left;
monitor_height := monitor.bottom - monitor.top;
// aspect_ratio := monitor_width / monitor_height;
w := cast(s32) (monitor_width * app_user_settings.window_width_fraction);
h := cast(s32) (monitor_height * app_user_settings.window_height_fraction);
x := cast(s32) (monitor.left + monitor_width / 2 - w / 2);
y := cast(s32) (monitor.top + monitor_height / 2 - h / 2);
return x, y, w, h;
}
// Back to the size and place the window starts at.
platform_reset_window_placement :: () -> bool { using platform;
// Displays may have been attached, removed or rearranged since startup.
monitors_enumerated = false;
monitor_index = 0;
for *monitors it.* = .{};
x, y, w, h := platform_get_centered_window_dimensions(open_on_biggest=true);
if IsZoomed(hwnd) != .FALSE ShowWindow(hwnd, SW_RESTORE); // otherwise it stays maximized at the new place
resetting_window_placement = true;
placed := SetWindowPos(hwnd, null, x, y, w, h, SWP_NOZORDER | SWP_NOACTIVATE) != .FALSE;
resetting_window_placement = false;
if !placed {
log_error("SetWindowPos failed; the window was not reset.");
os_log_error();
return false;
}
log("Window reset: x: %, y: %, size: %x% px", x, y, w, h);
return true;
}
// A key combination that types a character, such as Ctrl+Alt+Space, would also type it into the
// focused text field, and Alt+Space opens the window menu, so a bound key's character is dropped.
win32_key_down_is_bound :: (hotkey: Hotkey, virtual_key: WPARAM) -> bool {
key: ImGui.Key;
if virtual_key == VK_SPACE key = .Space;
else if virtual_key >= #char "A" && virtual_key <= #char "Z" key = cast(ImGui.Key) (cast(s64) ImGui.Key.A + cast(s64) (virtual_key - #char "A"));
else if virtual_key >= #char "0" && virtual_key <= #char "9" key = cast(ImGui.Key) (cast(s64) ImGui.Key._0 + cast(s64) (virtual_key - #char "0"));
else return false;
ctrl := GetKeyState(VK_CONTROL) < 0;
shift := GetKeyState(VK_SHIFT) < 0;
alt := GetKeyState(VK_MENU) < 0;
for array_view(hotkey.bindings, 0, hotkey.binding_count) {
if it.key == key && it.ctrl == ctrl && it.shift == shift && it.alt == alt return true;
}
return false;
}
get_primary_monitor :: () -> *Monitor {
if !monitors_enumerated {
enumerate_monitors();
monitors_enumerated = true;
}
for 0..monitors.count-1 {
if monitors[it].primary return *monitors[it];
}
return null;
}
// A monitor unplugged mid-enumeration fails GetMonitorInfoW; it is skipped, and only no monitors at
// all is fatal.
enumerate_monitors :: () {
EnumDisplayMonitors(null, null, monitor_enum_proc, 0);
if monitor_index == 0 panic("Couldn't enumerate monitors");
}
monitor_enum_proc :: (hMonitor: HMONITOR, hdc: HDC, rect: *RECT, data: LPARAM) -> BOOL #c_call {
using monitor : Monitor = ---;
monitor.left = rect.left;
monitor.top = rect.top;
monitor.right = rect.right;
monitor.bottom = rect.bottom;
monitorInfo.cbSize = size_of(MONITORINFO);
success := GetMonitorInfoW(hMonitor, *monitorInfo) != 0;
new_context : #Context;
use_app_logger(*new_context);
push_context new_context {
if !success {
log_error("[monitor_enum_proc] GetMonitorInfoW failed, monitor info unavailable; skipping that monitor.");
return xx true;
}
monitor.primary = !!(monitorInfo.dwFlags & 0x1);
monitor.present = true;
if monitor_index >= monitors.count {
log_error("[monitor_enum_proc] More than % monitors; ignoring the rest.", monitors.count);
return xx true;
}
monitors[monitor_index] = monitor;
monitor_index += 1;
}
return xx true;
}
// =-----------DATA-INTERNAL----------------------------------------------------= //
Monitors_Count :: () -> int {
count:=0; for monitors { count += xx it.present; }
return count;
}
monitors_enumerated : bool;
monitors: [8]Monitor;
monitor_index: int = 0;
Monitor :: struct {
monitorInfo: MONITORINFO;
left : int;
top : int;
right : int;
bottom : int;
primary: bool;
present: bool;
};
// =-----------CONSTANTS--------------------------------------------------------= //
THIS_ICON_ID :: 5001;
THIS_ICON_CONTEXT_MENU_ITEM_ID :: 5010;
LIVE_RESIZE_TIMER_ID :: 1;
// Windows raises anything below USER_TIMER_MINIMUM (10 ms) to it; Present's vsync paces the frames.
LIVE_RESIZE_TIMER_MS :: 1;
os_clear_unused_pages :: () {
SetProcessWorkingSetSize(GetCurrentProcess(), cast,no_check(SIZE_T)-1, cast,no_check(SIZE_T)-1);
log("[clear_unused_pages] Clearing unused pages.\n");
}
os_log_error :: () {
error_code := GetLastError();
log_error(" > GetLastError code: %, %", error_code, get_error_string(error_code));
}
// Asks for a folder with Explorer's own picker, over the app's window: its path (temp), or "" when
// none was chosen.
windows_choose_folder :: (title: string) -> string {
initialized := CoInitializeEx(null, COINIT_APARTMENTTHREADED);
defer if initialized >= 0 CoUninitialize();
dialog: *IFileOpenDialog;
if CoCreateInstance(*uid(CLSID_FileOpenDialog_UUID), null, CLSCTX_INPROC_SERVER, *uid(IFileOpenDialog_UUID), xx *dialog) < 0 return "";
defer IUnknown_Release(dialog);
options: u32;
dialog.GetOptions(dialog, *options);
dialog.SetOptions(dialog, options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);
wide_title := utf8_to_wide(title,, temp);
dialog.SetTitle(dialog, wide_title);
if dialog.Show(dialog, platform.hwnd) < 0 return ""; // cancelled
item: *IShellItem;
if dialog.GetResult(dialog, *item) < 0 return "";
defer IUnknown_Release(item);
name: *u16;
if item.GetDisplayName(item, SIGDN_FILESYSPATH, *name) < 0 return "";
defer CoTaskMemFree(name);
path, ok := wide_to_utf8(name,, temp);
return ifx ok then path else "";
}