Compare commits

...

3 Commits

Author SHA1 Message Date
Marcos Slomp
4f68dbcbbc workaround for X11 2026-06-12 14:18:37 -07:00
Marcos Slomp
3c90c2d7f8 removing RGFW cmake patch (fixed upstream) 2026-06-12 07:08:28 -07:00
Marcos Slomp
7e572c55fb testing platform abstraction via RGFW 2026-06-12 07:08:28 -07:00
5 changed files with 89 additions and 509 deletions

View File

@@ -78,12 +78,20 @@ if(APPLE)
endif()
# ---------------------------------------------------------------------------
# Platform-specific source and link settings
# Platform — RGFW (cross-platform windowing, fetched automatically)
# ---------------------------------------------------------------------------
set(PLATFORM_GENERATED_INCLUDES "")
include(FetchContent)
FetchContent_Declare(rgfw
GIT_REPOSITORY https://github.com/ColleagueRiley/RGFW.git
GIT_TAG main # pin to a specific commit for reproducible builds
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(rgfw)
set(PLATFORM_SOURCES platform/platform_rgfw.cpp)
set(PLATFORM_INCLUDES ${rgfw_SOURCE_DIR})
if(APPLE)
set(PLATFORM_SOURCES platform/platform_macos.mm)
set(PLATFORM_LIBS
"-framework Cocoa"
"-framework Metal"
@@ -92,45 +100,14 @@ if(APPLE)
"-framework IOKit"
"-framework IOSurface"
)
set_source_files_properties(platform/platform_macos.mm
PROPERTIES COMPILE_FLAGS "-ObjC++"
)
elseif(WIN32)
set(PLATFORM_SOURCES platform/platform_windows.cpp)
set(PLATFORM_LIBS user32 gdi32)
else()
# Linux / Wayland — generate xdg-shell protocol glue via wayland-scanner.
find_package(PkgConfig REQUIRED)
pkg_check_modules(WAYLAND_PROTOCOLS REQUIRED wayland-protocols)
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
find_program(WAYLAND_SCANNER wayland-scanner REQUIRED)
set(XDG_SHELL_XML "${WAYLAND_PROTOCOLS_DIR}/stable/xdg-shell/xdg-shell.xml")
set(XDG_SHELL_H "${CMAKE_CURRENT_BINARY_DIR}/xdg-shell-client-protocol.h")
set(XDG_SHELL_C "${CMAKE_CURRENT_BINARY_DIR}/xdg-shell-protocol.c")
add_custom_command(
OUTPUT "${XDG_SHELL_H}"
COMMAND "${WAYLAND_SCANNER}" client-header "${XDG_SHELL_XML}" "${XDG_SHELL_H}"
DEPENDS "${XDG_SHELL_XML}"
COMMENT "Generating xdg-shell-client-protocol.h"
VERBATIM
)
add_custom_command(
OUTPUT "${XDG_SHELL_C}"
COMMAND "${WAYLAND_SCANNER}" private-code "${XDG_SHELL_XML}" "${XDG_SHELL_C}"
DEPENDS "${XDG_SHELL_XML}"
COMMENT "Generating xdg-shell-protocol.c"
VERBATIM
)
set(PLATFORM_SOURCES
platform/platform_wayland.cpp
"${XDG_SHELL_C}"
"${XDG_SHELL_H}"
)
set(PLATFORM_LIBS wayland-client)
set(PLATFORM_GENERATED_INCLUDES "${CMAKE_CURRENT_BINARY_DIR}")
find_package(X11 REQUIRED)
if(NOT X11_Xrandr_FOUND)
message(FATAL_ERROR "Xrandr not found — install libxrandr-dev")
endif()
set(PLATFORM_LIBS X11::X11 X11::Xrandr)
endif()
# ---------------------------------------------------------------------------
@@ -163,7 +140,7 @@ endif()
target_include_directories(spinning_triangle PRIVATE
"${WGPU_PATH}/include"
"${TRACY_DIR}/public"
${PLATFORM_GENERATED_INCLUDES}
${PLATFORM_INCLUDES}
)
target_link_directories(spinning_triangle PRIVATE "${WGPU_PATH}/lib")

View File

@@ -1,121 +0,0 @@
// platform_macos.mm — macOS backend (Cocoa + CAMetalLayer)
//
// Compile flags (see spinning_triangle.cpp header for full invocation):
// -ObjC++ -framework Cocoa -framework Metal -framework QuartzCore \
// -framework Foundation -framework IOKit -framework IOSurface
#import <Cocoa/Cocoa.h>
#import <QuartzCore/CAMetalLayer.h>
#include <CoreFoundation/CFDate.h>
#include <webgpu/webgpu.h>
#include "platform.h"
static CAMetalLayer* sMetalLayer = nullptr;
static CFAbsoluteTime sStartTime = 0;
static void (*sRenderCb)() = nullptr;
static void (*sShutdownCb)() = nullptr;
// ---------------------------------------------------------------------------
// Cocoa app — window, metal layer, render timer
// ---------------------------------------------------------------------------
@interface AppDelegate : NSObject <NSApplicationDelegate, NSWindowDelegate>
@property (strong) NSWindow* window;
@property (strong) NSTimer* timer;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification*)notification {
// ~60 fps render loop
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 / 60.0
target:self
selector:@selector(tick:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown
handler:^NSEvent*(NSEvent* event) {
if (event.keyCode == 53) { // kVK_Escape
[NSApp terminate:nil];
return nil;
}
return event;
}];
[NSApp activateIgnoringOtherApps:YES];
[self.window makeKeyAndOrderFront:nil];
}
- (void)tick:(NSTimer*)t {
if (sRenderCb) sRenderCb();
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)app {
return YES;
}
- (void)applicationWillTerminate:(NSNotification*)notification {
[self.timer invalidate];
if (sShutdownCb) sShutdownCb();
}
@end
// ---------------------------------------------------------------------------
// Platform interface implementation
// ---------------------------------------------------------------------------
bool platformInit(int width, int height, const char* title) {
NSApplication* app = [NSApplication sharedApplication];
[app setActivationPolicy:NSApplicationActivationPolicyRegular];
NSRect frame = NSMakeRect(200, 200, width, height);
NSWindow* window = [[NSWindow alloc]
initWithContentRect:frame
styleMask:(NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable)
backing:NSBackingStoreBuffered
defer:NO];
[window setTitle:[NSString stringWithUTF8String:title]];
// Metal-backed layer
NSView* contentView = [window contentView];
[contentView setWantsLayer:YES];
sMetalLayer = [CAMetalLayer layer];
sMetalLayer.frame = contentView.bounds;
sMetalLayer.contentsScale = [window backingScaleFactor];
sMetalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
[contentView.layer addSublayer:sMetalLayer];
AppDelegate* del = [[AppDelegate alloc] init];
del.window = window;
[app setDelegate:del];
sStartTime = CFAbsoluteTimeGetCurrent();
return true;
}
WGPUSurface platformCreateSurface(WGPUInstance instance) {
WGPUSurfaceSourceMetalLayer metalSrc = {};
metalSrc.chain.sType = WGPUSType_SurfaceSourceMetalLayer;
metalSrc.layer = sMetalLayer;
WGPUSurfaceDescriptor surfDesc = {};
surfDesc.nextInChain = (WGPUChainedStruct*)&metalSrc;
return wgpuInstanceCreateSurface(instance, &surfDesc);
}
double platformGetTime() {
return CFAbsoluteTimeGetCurrent() - sStartTime;
}
void platformRunLoop(void (*render)(), void (*shutdown)()) {
sRenderCb = render;
sShutdownCb = shutdown;
@autoreleasepool {
[[NSApplication sharedApplication] run];
}
}

View File

@@ -0,0 +1,72 @@
// platform_rgfw.cpp — RGFW windowing backend for the WebGPU example
// https://github.com/ColleagueRiley/RGFW
#include "platform.h" // webgpu/webgpu.h first so RGFW sees WGPUSurface
#define RGFW_WEBGPU
#define RGFW_IMPLEMENTATION
#include <RGFW.h>
#include <chrono>
#include <cstdio>
#if defined(__linux__)
#include <X11/Xlib.h>
static bool platformHasDisplay() {
// RGFW workaround: RGFW indiscriminately passes XOpenDisplay(0) unchecked
// to X11 functions like XCreateWindow(), which will lead to SIGSEGV.
Display* display = XOpenDisplay(0);
if (display == nullptr) {
fprintf(stderr, "ERROR: failed to open X11 display (is $DISPLAY set?)\n");
return false;
}
XCloseDisplay(display);
return true;
}
#else
static bool platformHasDisplay() {
return true;
}
#endif
static RGFW_window* sWin = nullptr;
static std::chrono::steady_clock::time_point sStartTime;
bool platformInit(int width, int height, const char* title) {
if (!platformHasDisplay()) {
fprintf(stderr, "ERROR: no display found\n");
return false;
}
sWin = RGFW_createWindow(title, 0, 0, width, height, RGFW_windowCenter);
if (!sWin) {
fprintf(stderr, "ERROR: failed to create window\n");
return false;
}
RGFW_window_setExitKey(sWin, RGFW_keyEscape);
sStartTime = std::chrono::steady_clock::now();
return true;
}
WGPUSurface platformCreateSurface(WGPUInstance instance) {
return RGFW_window_createSurface_WebGPU(sWin, instance);
}
double platformGetTime() {
return std::chrono::duration<double>(
std::chrono::steady_clock::now() - sStartTime).count();
}
void platformRunLoop(void (*render)(), void (*shutdown)()) {
while (RGFW_window_shouldClose(sWin) == RGFW_FALSE) {
RGFW_event event;
while (RGFW_window_checkEvent(sWin, &event)) {
if (event.type == RGFW_windowClose) goto done;
}
render();
}
done:
shutdown();
RGFW_window_close(sWin);
sWin = nullptr;
}

View File

@@ -1,213 +0,0 @@
// platform_wayland.cpp — Linux/Wayland backend
//
// Dependencies:
// libwayland-client, wayland-protocols (for xdg-shell)
//
// Generate xdg-shell protocol glue before building:
// XML=$(pkg-config --variable=pkgdatadir wayland-protocols)/stable/xdg-shell/xdg-shell.xml
// wayland-scanner client-header $XML xdg-shell-client-protocol.h
// wayland-scanner private-code $XML xdg-shell-protocol.c
//
// Compile flags (see spinning_triangle.cpp header for full invocation):
// g++ -std=c++17 spinning_triangle.cpp platform_wayland.cpp \
// xdg-shell-protocol.c \
// -I/path/to/wgpu/include -L/path/to/wgpu/lib -lwgpu_native \
// $(pkg-config --cflags --libs wayland-client) \
// -o spinning_triangle
#include <wayland-client.h>
#include "xdg-shell-client-protocol.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <time.h>
#include <webgpu/webgpu.h>
#include "platform.h"
static wl_display* sDisplay = nullptr;
static wl_compositor* sCompositor = nullptr;
static xdg_wm_base* sWmBase = nullptr;
static wl_seat* sSeat = nullptr;
static wl_keyboard* sKeyboard = nullptr;
static wl_surface* sSurface = nullptr;
static xdg_surface* sXdgSurface = nullptr;
static xdg_toplevel* sToplevel = nullptr;
static bool sConfigured = false;
static bool sRunning = false;
static struct timespec sStartTime = {};
// ---------------------------------------------------------------------------
// xdg_wm_base listener — ping/pong keepalive
// ---------------------------------------------------------------------------
static void wmBasePing(void*, xdg_wm_base* wm, uint32_t serial) {
xdg_wm_base_pong(wm, serial);
}
static const xdg_wm_base_listener kWmBaseListener = { wmBasePing };
// ---------------------------------------------------------------------------
// xdg_surface listener — acknowledge configure events
// ---------------------------------------------------------------------------
static void xdgSurfaceConfigure(void*, xdg_surface* surf, uint32_t serial) {
xdg_surface_ack_configure(surf, serial);
sConfigured = true;
}
static const xdg_surface_listener kXdgSurfaceListener = { xdgSurfaceConfigure };
// ---------------------------------------------------------------------------
// xdg_toplevel listener — window close / resize
// ---------------------------------------------------------------------------
static void toplevelClose(void*, xdg_toplevel*) {
sRunning = false;
}
static void toplevelConfigure(void*, xdg_toplevel*, int32_t, int32_t, wl_array*) {}
static const xdg_toplevel_listener kToplevelListener = { toplevelConfigure, toplevelClose };
// ---------------------------------------------------------------------------
// Keyboard listener — Escape to quit
// ---------------------------------------------------------------------------
static void kbdKeymap(void*, wl_keyboard*, uint32_t, int32_t, uint32_t) {}
static void kbdEnter(void*, wl_keyboard*, uint32_t, wl_surface*, wl_array*) {}
static void kbdLeave(void*, wl_keyboard*, uint32_t, wl_surface*) {}
static void kbdKey(void*, wl_keyboard*, uint32_t, uint32_t, uint32_t key, uint32_t state) {
// key 1 == KEY_ESC in Linux evdev (linux/input-event-codes.h)
if (key == 1 && state == WL_KEYBOARD_KEY_STATE_PRESSED)
sRunning = false;
}
static void kbdModifiers(void*, wl_keyboard*, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) {}
static void kbdRepeatInfo(void*, wl_keyboard*, int32_t, int32_t) {}
static const wl_keyboard_listener kKbdListener = {
kbdKeymap, kbdEnter, kbdLeave, kbdKey, kbdModifiers, kbdRepeatInfo
};
// ---------------------------------------------------------------------------
// wl_seat listener — grab keyboard capability
// ---------------------------------------------------------------------------
static void seatCapabilities(void*, wl_seat* seat, uint32_t caps) {
if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !sKeyboard) {
sKeyboard = wl_seat_get_keyboard(seat);
wl_keyboard_add_listener(sKeyboard, &kKbdListener, nullptr);
} else if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD) && sKeyboard) {
wl_keyboard_release(sKeyboard);
sKeyboard = nullptr;
}
}
static void seatName(void*, wl_seat*, const char*) {}
static const wl_seat_listener kSeatListener = { seatCapabilities, seatName };
// ---------------------------------------------------------------------------
// Registry listener — bind global interfaces
// ---------------------------------------------------------------------------
static void registryGlobal(void*, wl_registry* reg,
uint32_t name, const char* iface, uint32_t ver) {
if (strcmp(iface, wl_compositor_interface.name) == 0)
sCompositor = (wl_compositor*)wl_registry_bind(reg, name, &wl_compositor_interface, 4);
else if (strcmp(iface, xdg_wm_base_interface.name) == 0) {
sWmBase = (xdg_wm_base*)wl_registry_bind(reg, name, &xdg_wm_base_interface, 1);
xdg_wm_base_add_listener(sWmBase, &kWmBaseListener, nullptr);
} else if (strcmp(iface, wl_seat_interface.name) == 0) {
sSeat = (wl_seat*)wl_registry_bind(reg, name, &wl_seat_interface, 5);
wl_seat_add_listener(sSeat, &kSeatListener, nullptr);
}
}
static void registryGlobalRemove(void*, wl_registry*, uint32_t) {}
static const wl_registry_listener kRegistryListener = { registryGlobal, registryGlobalRemove };
// ---------------------------------------------------------------------------
// Platform interface implementation
// ---------------------------------------------------------------------------
bool platformInit(int width, int height, const char* title) {
sDisplay = wl_display_connect(nullptr);
if (!sDisplay) { fprintf(stderr, "Cannot connect to Wayland display\n"); return false; }
wl_registry* registry = wl_display_get_registry(sDisplay);
wl_registry_add_listener(registry, &kRegistryListener, nullptr);
// Two roundtrips: first to enumerate globals, second for seat capabilities
wl_display_roundtrip(sDisplay);
wl_display_roundtrip(sDisplay);
if (!sCompositor) { fprintf(stderr, "No wl_compositor\n"); return false; }
if (!sWmBase) { fprintf(stderr, "No xdg_wm_base\n"); return false; }
sSurface = wl_compositor_create_surface(sCompositor);
sXdgSurface = xdg_wm_base_get_xdg_surface(sWmBase, sSurface);
sToplevel = xdg_surface_get_toplevel(sXdgSurface);
xdg_surface_add_listener(sXdgSurface, &kXdgSurfaceListener, nullptr);
xdg_toplevel_add_listener(sToplevel, &kToplevelListener, nullptr);
xdg_toplevel_set_title(sToplevel, title);
xdg_toplevel_set_app_id(sToplevel, "spinning_triangle");
wl_surface_commit(sSurface);
// Wait for the compositor to send the first configure
while (!sConfigured) wl_display_dispatch(sDisplay);
clock_gettime(CLOCK_MONOTONIC, &sStartTime);
return true;
}
WGPUSurface platformCreateSurface(WGPUInstance instance) {
WGPUSurfaceSourceWaylandSurface waylandSrc = {};
waylandSrc.chain.sType = WGPUSType_SurfaceSourceWaylandSurface;
waylandSrc.display = sDisplay;
waylandSrc.surface = sSurface;
WGPUSurfaceDescriptor surfDesc = {};
surfDesc.nextInChain = (WGPUChainedStruct*)&waylandSrc;
return wgpuInstanceCreateSurface(instance, &surfDesc);
}
double platformGetTime() {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (double)(now.tv_sec - sStartTime.tv_sec)
+ (double)(now.tv_nsec - sStartTime.tv_nsec) * 1e-9;
}
void platformRunLoop(void (*render)(), void (*shutdown)()) {
// Target ~16.67 ms per frame (60 fps)
static const long kFrameNs = 1000000000L / 60;
sRunning = true;
while (sRunning) {
struct timespec frameStart;
clock_gettime(CLOCK_MONOTONIC, &frameStart);
// Dispatch pending Wayland events without blocking
if (wl_display_dispatch_pending(sDisplay) < 0) break;
wl_display_flush(sDisplay);
if (sRunning) render();
// Sleep for the remainder of the frame budget
struct timespec frameEnd;
clock_gettime(CLOCK_MONOTONIC, &frameEnd);
long elapsed = (frameEnd.tv_sec - frameStart.tv_sec) * 1000000000L
+ (frameEnd.tv_nsec - frameStart.tv_nsec);
long remaining = kFrameNs - elapsed;
if (remaining > 0) {
struct timespec ts = { 0, remaining };
nanosleep(&ts, nullptr);
}
}
shutdown();
// Cleanup Wayland objects
if (sKeyboard) { wl_keyboard_release(sKeyboard); sKeyboard = nullptr; }
if (sToplevel) { xdg_toplevel_destroy(sToplevel); sToplevel = nullptr; }
if (sXdgSurface) { xdg_surface_destroy(sXdgSurface); sXdgSurface = nullptr; }
if (sSurface) { wl_surface_destroy(sSurface); sSurface = nullptr; }
if (sWmBase) { xdg_wm_base_destroy(sWmBase); sWmBase = nullptr; }
if (sSeat) { wl_seat_release(sSeat); sSeat = nullptr; }
if (sCompositor) { wl_compositor_destroy(sCompositor); sCompositor = nullptr; }
wl_display_disconnect(sDisplay);
}

View File

@@ -1,135 +0,0 @@
// platform_windows.cpp — Windows backend (Win32)
//
// Compile flags (MSVC, console subsystem):
// cl /std:c++17 spinning_triangle.cpp platform_windows.cpp \
// /I\path\to\wgpu\include \path\to\wgpu\lib\wgpu_native.lib \
// user32.lib gdi32.lib /Fe:spinning_triangle.exe
//
// MinGW/Clang equivalent:
// clang++ -std=c++17 spinning_triangle.cpp platform_windows.cpp \
// -I/path/to/wgpu/include -L/path/to/wgpu/lib -lwgpu_native \
// -luser32 -lgdi32 -o spinning_triangle.exe
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <webgpu/webgpu.h>
#include <stdio.h>
#include "platform.h"
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "dxguid.lib") // Dawn: WKPDID_D3DDebugObjectName
#pragma comment(lib, "OneCore") // Dawn: CompareObjectHandles
#pragma comment(lib, "ntdll.lib") // wgpu-native: NtReadFile et al.
static HWND sHwnd = nullptr;
static bool sRunning = false;
static LARGE_INTEGER sFreq = {};
static LARGE_INTEGER sStartTime = {};
// ---------------------------------------------------------------------------
// Win32 window procedure
// ---------------------------------------------------------------------------
static LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_KEYDOWN:
if (wp == VK_ESCAPE) { sRunning = false; return 0; }
break;
case WM_CLOSE:
case WM_DESTROY:
sRunning = false;
PostQuitMessage(0);
return 0;
}
return DefWindowProcA(hwnd, msg, wp, lp);
}
// ---------------------------------------------------------------------------
// Platform interface implementation
// ---------------------------------------------------------------------------
bool platformInit(int width, int height, const char* title) {
WNDCLASSEXA wc = {};
wc.cbSize = sizeof(wc);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = wndProc;
wc.hInstance = GetModuleHandleA(nullptr);
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.lpszClassName = "SpinningTriangle";
if (!RegisterClassExA(&wc)) {
fprintf(stderr, "RegisterClassExA failed (%lu)\n", GetLastError());
return false;
}
// Adjust client area to match the requested dimensions
RECT rect = { 0, 0, width, height };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW & ~(WS_THICKFRAME | WS_MAXIMIZEBOX), FALSE);
sHwnd = CreateWindowExA(
0, "SpinningTriangle", title,
WS_OVERLAPPEDWINDOW & ~(WS_THICKFRAME | WS_MAXIMIZEBOX),
CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top,
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
if (!sHwnd) {
fprintf(stderr, "CreateWindowExA failed (%lu)\n", GetLastError());
return false;
}
ShowWindow(sHwnd, SW_SHOW);
UpdateWindow(sHwnd);
QueryPerformanceFrequency(&sFreq);
QueryPerformanceCounter(&sStartTime);
return true;
}
WGPUSurface platformCreateSurface(WGPUInstance instance) {
WGPUSurfaceSourceWindowsHWND hwndSrc = {};
hwndSrc.chain.sType = WGPUSType_SurfaceSourceWindowsHWND;
hwndSrc.hinstance = GetModuleHandleA(nullptr);
hwndSrc.hwnd = sHwnd;
WGPUSurfaceDescriptor surfDesc = {};
surfDesc.nextInChain = (WGPUChainedStruct*)&hwndSrc;
return wgpuInstanceCreateSurface(instance, &surfDesc);
}
double platformGetTime() {
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
return (double)(now.QuadPart - sStartTime.QuadPart) / (double)sFreq.QuadPart;
}
void platformRunLoop(void (*render)(), void (*shutdown)()) {
// Target ~16.67 ms per frame (60 fps)
static const double kFrameTime = 1.0 / 60.0;
sRunning = true;
while (sRunning) {
double frameStart = platformGetTime();
// Drain the Win32 message queue
MSG msg;
while (PeekMessageA(&msg, nullptr, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) { sRunning = false; break; }
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
if (sRunning) render();
// Sleep for the remainder of the frame budget
double elapsed = platformGetTime() - frameStart;
if (elapsed < kFrameTime) {
DWORD ms = (DWORD)((kFrameTime - elapsed) * 1000.0);
if (ms > 0) Sleep(ms);
}
}
shutdown();
if (sHwnd) DestroyWindow(sHwnd);
}