Files
NX2navi/runtime/gfx_window_win.c
chrg6 29fe2b4396 Restyle the settings menu with the game's own interface fonts and textures, and give the window a gold pointer.
runtime/menu_assets.c reads them from the user's RomFS at runtime (nothing is shipped): .zs files with the dictionaries of Pack/ZsDic.pack.zs (streamed when a frame does not say its size), the fonts of Font2/Scalable behind their 8-byte header and per-word XOR key, and BNTX textures (Switch 2 archives say "Ounc" where Switch ones say "NX  ") through the runtime's block-linear copy and BC4 and ASTC decoders. ui/menu_art.cpp decodes the shared interface archive on a worker at the first present and uploads each texture for Dear ImGui at its first use. The menu now uses ClarimoUDPE for its text (with a sixth more line height) and CetraDisplayPro for its title and tabs, Roboto merged behind both; umber, brown and gold colours; the game's ornamented frame along the window's edge (the art holds two sides and is drawn again turned for the other two); its cursor glow and line around the selected row (edges only, so the label stays readable); and its control-guide bar under Continue and Quit. Without the files it keeps Roboto and plain shapes. The window's pointer is an original gold arrowhead drawn at start for the system DPI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:26:51 -05:00

458 lines
22 KiB
C

// The Win32 back end of the window (gfx_wsys.h): the window and its message loop on a thread of their own, F11
// fullscreen, the settings NX2NAVI_WINDOW_SIZE and NX2NAVI_FULLSCREEN, the monitor's refresh rate, focus, the Vulkan surface.
// The swapchain and the present are gfx_present.c's. The in-game menu's keyboard and mouse come from the window
// procedure (ui/menu_win.cpp), and so do the game's (input_win.c).
//
// The process is per-monitor DPI aware (nx2navi.manifest): every size here is physical pixels, NX2NAVI_WINDOW_SIZE included,
// and the swapchain matches the screen pixel for pixel. A window moved to a monitor at another display scale keeps its
// client size (WM_GETDPISCALEDSIZE); the menu takes its text size floor from the window's DPI (gfx_window_dpi).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <imm.h>
#include "cfg.h"
#include "flight.h"
#include "fs.h"
#include "gfx.h"
#include "gfx_wsys.h"
#include "input.h"
#include "input_win.h"
#include "nvn_api.h"
#include "ui/menu.h"
#include "vk.h"
#include "platform/platform.h"
#include <vulkan/vulkan_win32.h>
// ---- window state
static HWND g_hwnd;
static PlatEvent g_wnd_ready; // manual-reset: the window thread made the window or gave up
static int g_wnd_failed;
static int g_background; // NX2NAVI_BACKGROUND_WINDOW: the window stays behind the user's windows
static int g_fs_pending; // window thread: NX2NAVI_FULLSCREEN waits for the first activation
static volatile int32_t g_dpi = USER_DEFAULT_SCREEN_DPI; // the window's DPI, set by the window thread
HWND gfx_window_handle(void) { return g_hwnd; }
int gfx_window_vk_extensions(const char **out, int max) {
if (max < 2) return 0;
out[0] = VK_KHR_SURFACE_EXTENSION_NAME;
out[1] = VK_KHR_WIN32_SURFACE_EXTENSION_NAME;
return 2;
}
int gfx_window_vk_presentable(VkInstance inst, VkPhysicalDevice pd, uint32_t family) {
(void)inst;
return vkGetPhysicalDeviceWin32PresentationSupportKHR(pd, family) != VK_FALSE;
}
// ---- window thread
#define WND_STYLE WS_OVERLAPPEDWINDOW
#define WM_APP_SETTINGS (WM_APP + 1)
#define WM_APP_FULLSCREEN (WM_APP + 2) // the fullscreen setting, held back until the window is first activated
static WINDOWPLACEMENT g_saved_place = {sizeof g_saved_place}; // the window behind fullscreen (window thread)
static int is_fullscreen(HWND h) { return !(GetWindowLongW(h, GWL_STYLE) & WS_OVERLAPPEDWINDOW); }
#define cfg_fullscreen gfx_cfg_fullscreen
#define cfg_window_size gfx_cfg_window_size
unsigned gfx_window_dpi(void) { return (unsigned)plat_cas32(&g_dpi, 0, 0); }
// The outer size of a framed window whose client area is cw x ch. The frame is measured at the window's DPI, or the
// system DPI before the window exists: plain AdjustWindowRect always takes the system DPI, which is wrong on a monitor
// at another display scale.
static void outer_size(HWND h, int cw, int ch, int *ow, int *oh) {
RECT r = {0, 0, cw, ch};
AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, h ? GetDpiForWindow(h) : GetDpiForSystem());
*ow = r.right - r.left;
*oh = r.bottom - r.top;
}
// Windows turns the display off and sleeps after the idle timeout, and gamepad input does not count as activity. While
// the window is up and not minimized it holds the display and the system on, as other games do. The state belongs to
// the calling thread, so only the window thread (window_main and wnd_proc) calls this; the process exit releases it.
static void keep_awake(int on) {
static int cur = -1;
if (on == cur) return;
cur = on;
SetThreadExecutionState(on ? ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED : ES_CONTINUOUS);
}
// F11: borderless window over the whole monitor, and back to the saved window. The swapchain follows through WM_SIZE.
// Not Alt+Enter: Enter is the Plus button (input_win.c). A background window (test runs) keeps its place behind the
// user's windows and takes no focus.
static void toggle_fullscreen(HWND h) {
LONG style = GetWindowLongW(h, GWL_STYLE);
if (style & WS_OVERLAPPEDWINDOW) {
MONITORINFO mi = {sizeof mi};
if (!GetWindowPlacement(h, &g_saved_place) || !GetMonitorInfoW(MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY), &mi))
return;
SetWindowLongW(h, GWL_STYLE, (style & ~WS_OVERLAPPEDWINDOW) | WS_POPUP);
SetWindowPos(h, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
mi.rcMonitor.bottom - mi.rcMonitor.top,
SWP_NOOWNERZORDER | SWP_FRAMECHANGED | (g_background ? SWP_NOZORDER | SWP_NOACTIVATE : 0));
} else {
SetWindowLongW(h, GWL_STYLE, (style & ~WS_POPUP) | WND_STYLE);
if (g_background) g_saved_place.showCmd = SW_SHOWNOACTIVATE;
SetWindowPlacement(h, &g_saved_place);
SetWindowPos(h, NULL, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED |
(g_background ? SWP_NOACTIVATE : 0));
}
}
// The window size and fullscreen state last taken from the settings (window thread). apply_settings acts on a change of
// either value only, so a window resized by hand keeps its size when another setting changes.
static char g_applied_size[32];
static int g_applied_fs;
static void note_applied(void) {
snprintf(g_applied_size, sizeof g_applied_size, "%s", cfg_value("NX2NAVI_WINDOW_SIZE"));
g_applied_fs = cfg_fullscreen();
}
// Window thread: the window takes the size and fullscreen state of the settings when they changed. In fullscreen the
// size goes into the placement restored when fullscreen ends.
static void apply_settings(HWND h) {
int size_changed = strcmp(cfg_value("NX2NAVI_WINDOW_SIZE"), g_applied_size) != 0, fs = cfg_fullscreen(), fs_changed = fs != g_applied_fs;
note_applied();
if (size_changed) {
int cw, ch, ow, oh;
cfg_window_size(&cw, &ch);
outer_size(h, cw, ch, &ow, &oh);
if (is_fullscreen(h)) {
RECT *n = &g_saved_place.rcNormalPosition;
n->right = n->left + ow;
n->bottom = n->top + oh;
} else {
RECT have;
if (GetClientRect(h, &have) && (have.right != cw || have.bottom != ch)) {
if (IsZoomed(h)) ShowWindow(h, SW_RESTORE);
SetWindowPos(h, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
}
}
}
if (fs_changed && fs != is_fullscreen(h)) {
input_cursor_suspend(1);
toggle_fullscreen(h);
input_cursor_suspend(0);
}
}
void gfx_window_apply_settings(void) {
if (g_hwnd) PostMessageW(g_hwnd, WM_APP_SETTINGS, 0, 0);
}
void gfx_window_request_close(void) {
if (g_hwnd) PostMessageW(g_hwnd, WM_CLOSE, 0, 0);
}
// ---- refresh rate of the window's monitor (NX2NAVI_FPS_LIMIT=display), measured when the window opens, lands on another
// monitor or the display mode changes (gfx_window_set_refresh keeps it). QueryDisplayConfig gives the exact rational
// rate (59.94, 143.998); EnumDisplaySettings' whole hertz is the fallback.
static HMONITOR g_rate_mon;
static double monitor_rate_hz(HMONITOR mon) {
MONITORINFOEXW mi = {.cbSize = sizeof mi};
if (!GetMonitorInfoW(mon, (MONITORINFO *)&mi)) return 0;
double hz = 0;
UINT32 np = 0, nm = 0;
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &np, &nm) == ERROR_SUCCESS && np && nm) {
DISPLAYCONFIG_PATH_INFO *paths = calloc(np, sizeof *paths);
DISPLAYCONFIG_MODE_INFO *modes = calloc(nm, sizeof *modes);
if (paths && modes && QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &np, paths, &nm, modes, NULL) == ERROR_SUCCESS)
for (UINT32 i = 0; i < np && !hz; i++) {
DISPLAYCONFIG_SOURCE_DEVICE_NAME src = {.header = {.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
.size = sizeof src,
.adapterId = paths[i].sourceInfo.adapterId,
.id = paths[i].sourceInfo.id}};
DISPLAYCONFIG_RATIONAL r = paths[i].targetInfo.refreshRate;
if (DisplayConfigGetDeviceInfo(&src.header) == ERROR_SUCCESS && !wcscmp(src.viewGdiDeviceName, mi.szDevice) &&
r.Denominator)
hz = (double)r.Numerator / r.Denominator;
}
free(paths);
free(modes);
}
DEVMODEW dm = {.dmSize = sizeof dm};
if (!hz && EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1)
hz = dm.dmDisplayFrequency; // 0 and 1 mean the hardware default
return hz;
}
// Window thread. force: the display mode changed, measure even on the same monitor.
static void refresh_rate_update(HWND h, int force) {
HMONITOR mon = MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY);
if (!force && mon == g_rate_mon) return;
g_rate_mon = mon;
gfx_window_set_refresh(monitor_rate_hz(mon));
}
// Before the window exists (widescreen.c wide_init at start), the primary monitor: the one a fullscreen window opens on.
int gfx_window_monitor_size(int *w, int *h) {
MONITORINFO mi = {sizeof mi};
HMONITOR mon = g_hwnd ? MonitorFromWindow(g_hwnd, MONITOR_DEFAULTTOPRIMARY) : MonitorFromPoint((POINT){0, 0}, MONITOR_DEFAULTTOPRIMARY);
if (!GetMonitorInfoW(mon, &mi)) return 0;
*w = mi.rcMonitor.right - mi.rcMonitor.left;
*h = mi.rcMonitor.bottom - mi.rcMonitor.top;
return 1;
}
static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
LRESULT input_res;
if (input_window_msg(h, m, w, l, &input_res)) return input_res;
if (menu_window_msg(h, m, w, l, &input_res)) return input_res;
switch (m) {
case WM_KEYDOWN:
if (w == VK_F11 && !(l & (1 << 30))) { // bit 30: auto-repeat
input_cursor_suspend(1);
toggle_fullscreen(h);
input_cursor_suspend(0);
cfg_set("NX2NAVI_FULLSCREEN", is_fullscreen(h) ? "1" : "0"); // the menu shows it; the next start keeps it
cfg_save();
note_applied(); // what cfg_get reads now (the environment may still override it)
}
break;
case WM_APP_SETTINGS:
apply_settings(h);
return 0;
case WM_APP_FULLSCREEN:
if (cfg_fullscreen() && !is_fullscreen(h)) {
input_cursor_suspend(1);
toggle_fullscreen(h);
input_cursor_suspend(0);
}
return 0;
case WM_SIZE:
keep_awake(w != SIZE_MINIMIZED);
if (w != SIZE_MINIMIZED) gfx_window_mark_dirty();
refresh_rate_update(h, 0);
return 0;
case WM_EXITSIZEMOVE: // dragged onto another monitor
refresh_rate_update(h, 0);
break;
case WM_DISPLAYCHANGE:
refresh_rate_update(h, 1);
break;
case WM_GETDPISCALEDSIZE: // a framed window keeps its client size in pixels on a monitor at another scale
if (!is_fullscreen(h) && !IsZoomed(h)) {
RECT c, r;
if (GetClientRect(h, &c)) {
r = (RECT){0, 0, c.right, c.bottom};
if (AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, LOWORD(w))) {
*(SIZE *)l = (SIZE){r.right - r.left, r.bottom - r.top};
return TRUE;
}
}
}
break; // DefWindowProc: Windows scales the size with the DPI
case WM_DPICHANGED: { // display scale changed, or the window reached a monitor at another scale
const RECT *s = (const RECT *)l;
plat_xchg32(&g_dpi, LOWORD(w));
rt_log("gfx: window DPI %u (%u%%)", LOWORD(w), LOWORD(w) * 100u / USER_DEFAULT_SCREEN_DPI);
if (is_fullscreen(h)) { // keep covering the monitor the suggested rect lies on
MONITORINFO mi = {sizeof mi};
if (GetMonitorInfoW(MonitorFromRect(s, MONITOR_DEFAULTTONEAREST), &mi))
SetWindowPos(h, NULL, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_NOZORDER | SWP_NOACTIVATE);
} else {
SetWindowPos(h, NULL, s->left, s->top, s->right - s->left, s->bottom - s->top, SWP_NOZORDER | SWP_NOACTIVATE);
}
return 0;
}
case WM_ACTIVATEAPP: {
int in = w != 0;
gfx_window_focus_changed(in);
if (in && g_fs_pending) {
g_fs_pending = 0;
PostMessageW(h, WM_APP_FULLSCREEN, 0, 0);
}
return 0;
}
case WM_CLOSE:
gfx_window_closed();
case WM_ERASEBKGND:
return 1;
}
return DefWindowProcW(h, m, w, l);
}
// Brings the game window to the front. Windows refuses SetForegroundWindow to a process that was not the last to get
// input, which is the case for a game started by a frontend; attaching this thread's input queue to the foreground
// window's thread for the call is the documented way around it. The window is not left topmost.
static void raise_window(HWND h) {
HWND fg = GetForegroundWindow();
DWORD me = GetCurrentThreadId(), other = fg ? GetWindowThreadProcessId(fg, NULL) : 0;
int attached = other && other != me && AttachThreadInput(me, other, TRUE);
ShowWindow(h, IsIconic(h) ? SW_RESTORE : SW_SHOW);
BringWindowToTop(h);
SetForegroundWindow(h);
SetActiveWindow(h);
if (attached) AttachThreadInput(me, other, FALSE);
if (GetForegroundWindow() != h) { // still refused: a topmost toggle puts it above the others without keeping it there
SetWindowPos(h, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
SetWindowPos(h, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
}
}
// The window's pointer: a gold arrowhead in the menu's colours, its tip the hotspot at the top left, the half towards
// the tip's right edge a lighter facet, with a dark outline; sized for the system DPI (32 px at 96 DPI) and drawn with
// 4x4 samples per pixel into a 32-bit cursor with alpha. The arrow when anything fails.
static int in_tri(float px, float py, float ax, float ay, float bx, float by, float cx, float cy) {
float d1 = (px - bx) * (ay - by) - (ax - bx) * (py - by), d2 = (px - cx) * (by - cy) - (bx - cx) * (py - cy),
d3 = (px - ax) * (cy - ay) - (cx - ax) * (py - ay);
int neg = d1 < 0 || d2 < 0 || d3 < 0, pos = d1 > 0 || d2 > 0 || d3 > 0;
return !(neg && pos);
}
static HCURSOR pointer_cursor(void) {
UINT dpi = GetDpiForSystem();
int n = (int)(32 * (dpi ? dpi : 96) / 96);
if (n < 24) n = 24;
if (n > 96) n = 96;
BITMAPV5HEADER bh = {.bV5Size = sizeof bh, .bV5Width = n, .bV5Height = -n, .bV5Planes = 1, .bV5BitCount = 32,
.bV5Compression = BI_BITFIELDS, .bV5RedMask = 0x00ff0000, .bV5GreenMask = 0x0000ff00,
.bV5BlueMask = 0x000000ff, .bV5AlphaMask = 0xff000000};
HDC dc = GetDC(NULL);
uint32_t *px = NULL;
HBITMAP color = CreateDIBSection(dc, (BITMAPINFO *)&bh, DIB_RGB_COLORS, (void **)&px, NULL, 0);
ReleaseDC(NULL, dc);
HBITMAP mask = CreateBitmap(n, n, 1, 1, NULL);
if (!color || !mask || !px) {
if (color) DeleteObject(color);
if (mask) DeleteObject(mask);
return LoadCursorW(NULL, (LPCWSTR)IDC_ARROW);
}
// the arrowhead: tip (ax, ay), the right wing (bx, by), the lower wing (cx, cy), the notch (mx, my) between them
float o = n * 0.07f, ax = o, ay = o, bx = o + n * 0.70f, by = o + n * 0.46f, cx = o + n * 0.30f, cy = o + n * 0.80f;
float mx = o + n * 0.36f, my = o + n * 0.50f, e = n * 0.055f;
for (int y = 0; y < n; y++)
for (int x = 0; x < n; x++) {
int light = 0, deep = 0, edge = 0;
for (int sy = 0; sy < 4; sy++)
for (int sx = 0; sx < 4; sx++) {
float fx = x + (sx + 0.5f) / 4, fy = y + (sy + 0.5f) / 4;
int l = in_tri(fx, fy, ax, ay, bx, by, mx, my), d = !l && in_tri(fx, fy, ax, ay, mx, my, cx, cy);
int out = in_tri(fx, fy, ax - e, ay - e * 1.3f, bx + e * 1.4f, by + e * 0.2f, mx, my + e * 1.2f) ||
in_tri(fx, fy, ax - e * 1.3f, ay - e, mx + e * 1.2f, my, cx + e * 0.2f, cy + e * 1.4f);
light += l, deep += d, edge += out && !l && !d;
}
int cov = light + deep + edge;
uint32_t a = (uint32_t)(cov * 255 / 16), r = 0, g = 0, b = 0;
if (cov) {
r = (uint32_t)((255 * light + 214 * deep + 44 * edge) / cov);
g = (uint32_t)((226 * light + 158 * deep + 28 * edge) / cov);
b = (uint32_t)((138 * light + 58 * deep + 10 * edge) / cov);
}
// premultiplied by alpha, as 32-bit cursors want
px[y * n + x] = a << 24 | (r * a / 255) << 16 | (g * a / 255) << 8 | (b * a / 255);
}
ICONINFO ii = {.fIcon = FALSE, .xHotspot = (DWORD)o, .yHotspot = (DWORD)o, .hbmMask = mask, .hbmColor = color};
HCURSOR cur = CreateIconIndirect(&ii);
DeleteObject(color);
DeleteObject(mask);
return cur ? cur : LoadCursorW(NULL, (LPCWSTR)IDC_ARROW);
}
static void window_main(void *arg) {
(void)arg;
HINSTANCE inst = GetModuleHandleW(NULL);
// Icon resource 1 of nx2navi.rc (runtime/nx2navi.ico): the title bar, the taskbar button and Alt+Tab.
WNDCLASSEXW wc = {.cbSize = sizeof wc, .lpfnWndProc = wnd_proc, .hInstance = inst,
.hIcon = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXICON),
GetSystemMetrics(SM_CYICON), LR_SHARED),
.hIconSm = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), LR_SHARED),
.hCursor = pointer_cursor(), .lpszClassName = L"NX2naviRecompWindow"};
RegisterClassExW(&wc);
int cw, ch, ow, oh;
cfg_window_size(&cw, &ch);
outer_size(NULL, cw, ch, &ow, &oh);
// The window opens behind the user's windows first and is then raised (raise_window): a game started by a frontend
// has no other way to reach it. The game counts itself in focus until the first WM_ACTIVATEAPP, so loading goes on,
// and keyboard input waits for the foreground (input_win.c). If Windows still refuses the foreground, the taskbar
// button flashes, and NX2NAVI_FULLSCREEN waits for the first activation.
// NX2NAVI_BACKGROUND_WINDOW=1 (test runs): no flashing, never fullscreen at start, and F11 keeps the window's place.
int background = g_background = cfg_get("NX2NAVI_BACKGROUND_WINDOW") && atoi(cfg_get("NX2NAVI_BACKGROUND_WINDOW"));
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"Ocarina Recomp", WND_STYLE, CW_USEDEFAULT, CW_USEDEFAULT, ow, oh, NULL,
NULL, inst, NULL);
if (!g_hwnd) {
rt_log("gfx: CreateWindowEx failed (%lu)", GetLastError());
g_wnd_failed = 1;
} else {
// An IME switched on over the game attaches to this window and then shows its candidate window and eats the keys
// it composes, although there is nothing here to type into (a Chinese IME takes Shift, the B button by default).
// Detaching the game's window leaves the software keyboard alone: its edit box sits in a window of its own
// (swkbd_win.c) and gets the IME it needs.
ImmAssociateContext(g_hwnd, NULL);
// The frame was measured at the system DPI; the window may have opened on a monitor at another scale.
plat_xchg32(&g_dpi, (int32_t)GetDpiForWindow(g_hwnd));
outer_size(g_hwnd, cw, ch, &ow, &oh);
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
if (!background) {
raise_window(g_hwnd); // a frontend's player may have no way to reach a window left behind
FLASHWINFO fw = {.cbSize = sizeof fw, .hwnd = g_hwnd, .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG};
FlashWindowEx(&fw);
g_fs_pending = cfg_fullscreen();
}
note_applied();
refresh_rate_update(g_hwnd, 1);
keep_awake(1);
}
plat_event_set(&g_wnd_ready);
if (g_wnd_failed) return;
MSG msg;
while (GetMessageW(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
return;
}
// ---- gfx_wsys.h
// Makes the window on its thread and waits until it is there.
int wsys_open(void) {
g_wnd_failed = 0;
plat_event_init(&g_wnd_ready, 1, 0);
if (!plat_thread_spawn(window_main, NULL, "window", 0)) rt_fatal("gfx: cannot start the window thread");
plat_event_wait(&g_wnd_ready);
return !g_wnd_failed;
}
VkSurfaceKHR wsys_surface(VkInstance inst) {
VkWin32SurfaceCreateInfoKHR si = {.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR,
.hinstance = GetModuleHandleW(NULL), .hwnd = g_hwnd};
VkSurfaceKHR surface = VK_NULL_HANDLE;
VkResult r = vkCreateWin32SurfaceKHR(inst, &si, NULL, &surface);
if (r != VK_SUCCESS) rt_log("gfx: vkCreateWin32SurfaceKHR failed (%d)", (int)r);
return r == VK_SUCCESS ? surface : VK_NULL_HANDLE;
}
void wsys_client_size(int *w, int *h) {
RECT rc = {0};
GetClientRect(g_hwnd, &rc);
*w = rc.right, *h = rc.bottom;
}
int wsys_minimized(void) { return IsIconic(g_hwnd) != 0; }
void wsys_resize(int cw, int ch) {
int ow, oh;
outer_size(g_hwnd, cw, ch, &ow, &oh);
SetWindowPos(g_hwnd, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
}
void wsys_probe_focus(int in) { SendMessageW(g_hwnd, WM_ACTIVATEAPP, in ? TRUE : FALSE, 0); }
void wsys_probe_escape(void) { SendMessageW(g_hwnd, WM_KEYDOWN, VK_ESCAPE, 0); }
int wsys_probe_click(int where) {
if (where == 0) SendMessageW(g_hwnd, WM_LBUTTONDOWN, MK_LBUTTON, 0);
else SendMessageW(g_hwnd, WM_MOUSEACTIVATE, (WPARAM)g_hwnd, MAKELPARAM(where == 1 ? HTCLIENT : HTCAPTION, WM_LBUTTONDOWN));
return 1;
}