696 lines
26 KiB
Plaintext
696 lines
26 KiB
Plaintext
main :: () {
|
|
using platform; using ImGui;
|
|
// #Startup: Program Startup
|
|
app.args = get_command_line_arguments();
|
|
print_version_and_exit := array_find(app.args, "-version");
|
|
|
|
if print_version_and_exit {
|
|
print("Version: %-%-%, built on %", VERSION, GIT_BRANCH, GIT_HASH, BUILD_DATE);
|
|
return;
|
|
}
|
|
|
|
load_hardware_information();
|
|
load_platform_information();
|
|
|
|
args_combined := join(..app.args, " ");
|
|
app.run_tests = array_find(app.args, "run_tests");
|
|
app.headless = array_find(app.args, "-headless") || NO_CRASH_DIALOG;
|
|
runtime_support_headless = app.headless;
|
|
app.allow_multiple_instances = array_find(app.args, "-allow-multiple-instances");
|
|
|
|
defer report_memory_leaks();
|
|
|
|
channel_key := app_shared_memory_channel_key();
|
|
defer free(channel_key);
|
|
|
|
// 1. Check file lock on POSIX machines:
|
|
app_instance_running : bool;
|
|
#if USING_SDL {
|
|
lock_path := app_instance_lock_path();
|
|
lock_fd: s32 = -1;
|
|
if lock_path {
|
|
// 600 in base 8 is 0x180. O_NOFOLLOW, so a planted symlink is not truncated below.
|
|
lock_fd = POSIX.open(temp_c_string(lock_path), POSIX.O_CREAT|POSIX.O_RDWR|POSIX.O_CLOEXEC|POSIX.O_NOFOLLOW, 0x180);
|
|
if lock_fd < 0 log_error("Failed to open the instance lock %: errno %", lock_path, POSIX.errno());
|
|
} else {
|
|
log_error("No per-user directory is available for the instance lock.");
|
|
}
|
|
defer if lock_fd >= 0 POSIX.close(lock_fd);
|
|
|
|
lock_taken := false;
|
|
if lock_fd >= 0 {
|
|
// Set up the flock structure for an exclusive write lock:
|
|
fl: POSIX.flock;
|
|
fl.l_type = POSIX.F_WRLCK; // exclusive write lock
|
|
fl.l_whence = POSIX.SEEK_SET;
|
|
fl.l_start = 0; // lock from start of file
|
|
fl.l_len = 0; // lock until EOF
|
|
|
|
// Try to lock the file. F_SETLK returns -1 immediately if locked.
|
|
if POSIX.fcntl(lock_fd, POSIX.F_SETLK, *fl) == -1 {
|
|
if (POSIX.errno() == POSIX.EACCES || POSIX.errno() == POSIX.EAGAIN) {
|
|
log("app_instance_running already!");
|
|
app_instance_running = true;
|
|
} else {
|
|
log_error("fcntl on the instance lock failed: errno %", POSIX.errno());
|
|
}
|
|
} else {
|
|
lock_taken = true;
|
|
}
|
|
}
|
|
|
|
if !lock_taken && !app_instance_running {
|
|
log_error("Could not take the instance lock; running without single-instance support.");
|
|
app.allow_multiple_instances = true;
|
|
}
|
|
|
|
if lock_taken {
|
|
// remove stale reference if needed:
|
|
POSIX.shm_unlink(temp_c_string(channel_key));
|
|
// Truncate the file and write PID:
|
|
if POSIX.ftruncate(lock_fd, 0) == 0 {
|
|
pid := POSIX.getpid();
|
|
pid_str := tprint("%\n", pid);
|
|
POSIX.write(lock_fd, pid_str.data, cast(u64)pid_str.count);
|
|
}
|
|
}
|
|
}
|
|
|
|
if !app.allow_multiple_instances {
|
|
// #Startup:#shared_memory_channel:bringToFront
|
|
// One primitive decides which instance is master: the lock on POSIX, and the creation of the
|
|
// mapping on Windows. Whoever loses only ever connects, retrying while the winner may still
|
|
// be setting its channel up, and then exits.
|
|
another_instance_owns_channel := false;
|
|
#if USING_SDL another_instance_owns_channel = app_instance_running;
|
|
|
|
if !another_instance_owns_channel && app_bring_running_instance_to_front(channel_key, attempts=1) return;
|
|
|
|
if !another_instance_owns_channel {
|
|
read_channel_success, read_channel, already_exists :=
|
|
Shared_Channel.reader_create_channel(channel_key, APP_SHARED_MEMORY_CHANNEL_BYTES);
|
|
if read_channel_success {
|
|
app.read_channel = read_channel;
|
|
app.is_master = true;
|
|
} else if already_exists {
|
|
another_instance_owns_channel = true;
|
|
} else {
|
|
// Not fatal: this instance runs without the channel, so later launches will not bring it forward.
|
|
log_error("Failed to create the shared memory channel; continuing without single-instance support.");
|
|
}
|
|
}
|
|
|
|
if another_instance_owns_channel {
|
|
if !app_bring_running_instance_to_front(channel_key, attempts=20) {
|
|
log_error("Another instance is running, but its shared memory channel could not be reached (it may be running as administrator); exiting.");
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
defer if app.is_master Shared_Channel.reader_release_channel(*app.read_channel, channel_key);
|
|
|
|
crash_report_folder_name = APP_FOLDER_NAME;
|
|
init_logger(); defer close_logger();
|
|
|
|
log(" --- /STARTUP INFO\\ --- ");
|
|
log(" - APP_NAME: %", APP_NAME);
|
|
log(" - VERSION: %", VERSION);
|
|
log(" - DEBUG: %", DEBUG);
|
|
log(" - BUILD_DATE: %", BUILD_DATE);
|
|
log(" - GIT_BRANCH: %", GIT_BRANCH);
|
|
log(" - GIT_HASH: %", GIT_HASH);
|
|
log(" - COMPILE_ARGS: %", COMPILE_ARGS);
|
|
log(" - COMMAND_LINE_ARGUMENTS: %", args_combined);
|
|
log(" --- \\STARTUP INFO/ --- ");
|
|
|
|
app.startup_time = current_time_monotonic();
|
|
resize(*perf.frame_times, PERF_FRAME_TIME_HISTORY,, context.default_allocator);
|
|
timing_set_tick_frequency();
|
|
|
|
if app.run_tests {
|
|
#if COMPILE_TESTS {
|
|
// Headless, a failed suite is counted and reported in the exit code rather than asserted on.
|
|
testing_init(assert_on_fail = DEBUG && !app.headless, log_successful_tests=DEBUG);
|
|
run_all_tests();
|
|
} else {
|
|
log_error("run_tests was passed, but this build was compiled without tests. Rebuild with the compile_tests argument to include them.");
|
|
}
|
|
}
|
|
|
|
#if USING_WINDOWS {
|
|
windows_check_if_we_are_running_as_administrator();
|
|
if app.should_exit_early then return;
|
|
windows_enable_large_pages_if_possible();
|
|
}
|
|
|
|
#if OS == .MACOS {
|
|
init_mac_app();
|
|
}
|
|
|
|
app.machine_name = copy_string(get_machinename());
|
|
app.username = copy_string(get_username());
|
|
log("Current PC name: %, username: %", app.machine_name, app.username);
|
|
|
|
settings_write_back := app_load_user_settings_file();
|
|
logger_set_event_log_capacity(app_user_settings.event_log_capacity);
|
|
|
|
// #Startup:CreateWindow
|
|
#if USING_SDL {
|
|
// Keep freetype from being removed by the compiler
|
|
FT_Error_String(0);
|
|
// Setup window and gpu using the SDL3_GPU3 backend.
|
|
// This is based on the 'example_sdl3_sdlgpu3' Dear ImGui example, which you can find on their repo.
|
|
success := SDL_Init(SDL_INIT_VIDEO);
|
|
#if OS == .MACOS { // By default Cmd+W closes the application if the last window exits, so we need to tell SDL to NOT do that.
|
|
SDL_SetHint(SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE, "0");
|
|
}
|
|
ok_or_exit(success, "failed to init sdl3 with error = %", consume_sdl_error());
|
|
|
|
display := sdl_largest_display();
|
|
app.win_scale = SDL_GetDisplayContentScale(display);
|
|
if app.win_scale <= 0 app.win_scale = 1.0; // 0 when SDL cannot tell, as on some Linux setups
|
|
mode := SDL_GetCurrentDisplayMode(display);
|
|
|
|
w, h := sdl_default_window_size(mode);
|
|
|
|
window_flags : SDL_WindowFlags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN | SDL_WINDOW_HIGH_PIXEL_DENSITY;
|
|
|
|
platform.window = SDL_CreateWindow(APP_WINDOW_TITLE, w, h, window_flags);
|
|
ok_or_exit(platform.window != null, "failed to create window with error = %", consume_sdl_error());
|
|
|
|
centered := SDL_WINDOWPOS_CENTERED_DISPLAY(display);
|
|
SDL_SetWindowPosition(platform.window, xx centered, xx centered);
|
|
sdl_set_app_icon();
|
|
#if OS == .MACOS {
|
|
SDL_AddEventWatch(sdl_live_resize_watch, null);
|
|
}
|
|
SDL_ShowWindow(platform.window);
|
|
} // #if USING_SDL
|
|
#if USING_WINDOWS {
|
|
ImGui.ImplWin32_EnableDpiAwareness();
|
|
|
|
app.win_scale = ImGui.ImplWin32_GetDpiScaleForMonitor(MonitorFromPoint({0,0}, MONITOR_DEFAULTTOPRIMARY));
|
|
|
|
lpszClassName :: #run tprint("%WindowClass", APP_NAME);
|
|
wc: WNDCLASSEXW;
|
|
wc.cbSize = size_of(WNDCLASSEXW);
|
|
wc.style = CS_HREDRAW | CS_VREDRAW;
|
|
wc.lpfnWndProc = xx WndProc;
|
|
wc.cbClsExtra = 0;
|
|
wc.cbWndExtra = 0;
|
|
wc.hInstance = null;
|
|
wc.hIcon = LoadImageW(null, utf8_to_wide(get_app_tray_icon_path(),,temp), IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
|
|
wc.hCursor = LoadCursorW(null, IDC_ARROW);
|
|
wc.hbrBackground = null;
|
|
wc.lpszMenuName = null;
|
|
wc.lpszClassName = utf8_to_wide(lpszClassName,, context.default_allocator);
|
|
|
|
if RegisterClassExW(*wc) == 0 {
|
|
log("RegisterClassExW failed\n");
|
|
return;
|
|
}
|
|
defer {
|
|
UnregisterClassW(wc.lpszClassName, wc.hInstance);
|
|
free(wc.lpszClassName,, context.default_allocator);
|
|
}
|
|
|
|
// Get primary display height and width
|
|
window_x, window_y, win_width, win_height := platform_get_centered_window_dimensions(open_on_biggest=true);
|
|
log("Window Location: x: %, y: %, size: %x% px", window_x, window_y, win_width, win_height);
|
|
|
|
hwnd = CreateWindowExW(WS_EX_APPWINDOW,
|
|
utf8_to_wide(lpszClassName,,temp),
|
|
utf8_to_wide(APP_WINDOW_TITLE,,temp),
|
|
WS_OVERLAPPEDWINDOW,
|
|
window_x, window_y, win_width, win_height,
|
|
null, null, null, null);
|
|
if !hwnd {
|
|
print("CreateWindowExW failed\n");
|
|
return;
|
|
}
|
|
defer DestroyWindow(hwnd);
|
|
} // #if USING_WINDOWS
|
|
|
|
// #Startup:Tray #Startup:GlobalHotkeys
|
|
#if USING_SDL {
|
|
posix_install_exit_signal_handlers();
|
|
#if OS == .LINUX {
|
|
linux_platform_setup();
|
|
}
|
|
}
|
|
#if USING_WINDOWS { // :Win32_Initialize_Extended
|
|
Win32_Set_Tray_Icon();
|
|
Win32_Hide_Tray_Icon();
|
|
defer Win32_Hide_Tray_Icon(); // exiting from the tray would otherwise leave the icon behind
|
|
}
|
|
// [global_hotkeys] was read with the settings, before there was a window to register them for.
|
|
global_hotkeys_apply();
|
|
defer platform_unregister_global_hotkeys();
|
|
// #Startup:Graphics
|
|
#if USING_SDL {
|
|
new_device := SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_METALLIB, DEBUG, null);
|
|
ok_or_exit(new_device != null, "failed to create gpu device with error = %", consume_sdl_error());
|
|
platform.gpu_device = new_device;
|
|
|
|
success = SDL_ClaimWindowForGPUDevice(gpu_device, platform.window);
|
|
ok_or_exit(success, "failed to claim window with title '%' for gpu device with error = %", to_string(SDL_GetWindowTitle(platform.window)), consume_sdl_error());
|
|
|
|
success = SDL_SetGPUSwapchainParameters(gpu_device, platform.window, .SDR, .VSYNC);
|
|
ok_or_exit(success, "failed to set swapchain parameters with error = %", consume_sdl_error());
|
|
|
|
perf.renderer_name = copy_string(to_string(SDL_GetGPUDeviceDriver(gpu_device)));
|
|
log("SDL renderer_name: %", perf.renderer_name);
|
|
}
|
|
#if USING_WINDOWS {
|
|
// #Startup:Graphics See #WarpFirstStartup in graphics/DX11.jai.
|
|
if !D3D11_InitDevice(hwnd, .WARP) {
|
|
log_error("Could not create a D3D11 device of any kind. Exiting.");
|
|
return;
|
|
}
|
|
perf.renderer_name = "direct3d11";
|
|
perf.started_on_warp = (g_driver_type == .WARP);
|
|
log("[D3D11] Renderer up on '%' (%): device % ms, swap chain % ms.", perf.adapter_name, g_driver_type,
|
|
FF(perf.startup_device_ms, 1), FF(perf.startup_swap_chain_ms, 1));
|
|
|
|
// Started hardware device thread after WARP initialied, as in the reference implementation: https://gist.github.com/thomasklemenc/2d4d43001a977a3dafe4de73efc348da
|
|
// Running both simultaneously causes contention with the loader lock and DXGI's internal locks, and the resulting init time is 100ms+
|
|
if perf.started_on_warp then d3d11_start_hardware_device_thread();
|
|
defer d3d11_finish_hardware_device_thread();
|
|
|
|
ShowWindow(hwnd, SW_SHOWDEFAULT);
|
|
UpdateWindow(hwnd);
|
|
|
|
// Set dark titlebar:
|
|
dark_mode: s64 = 1;
|
|
DwmSetWindowAttribute(hwnd, .USE_IMMERSIVE_DARK_MODE, *dark_mode, size_of(s64));
|
|
// A hacky workaround to display the dark title bar properly. Nothing else we've tried worked (credit: focus-editor)
|
|
ShowWindow(hwnd, SW_HIDE);
|
|
ShowWindow(hwnd, SW_SHOW);
|
|
}
|
|
|
|
perf.backend = graphics_backend_from_string(perf.renderer_name);
|
|
|
|
// #Startup:ImGui Graphics Backend
|
|
app_imgui_init();
|
|
|
|
#if USING_SDL {
|
|
io := GetIO();
|
|
|
|
// Enable freetype rendering
|
|
io.Fonts.SetFontLoader(io.Fonts, FreeType.GetFontLoader());
|
|
|
|
// Setup imgui sdl3_gpu3 backend
|
|
success = ImplSDL3_InitForSDLGPU(platform.window);
|
|
ok_or_exit(success, "failed to InitForSDLGPU with error = %", consume_sdl_error());
|
|
|
|
init_info := ImplSDLGPU3_InitInfo.{
|
|
Device = gpu_device,
|
|
ColorTargetFormat = SDL_GetGPUSwapchainTextureFormat(gpu_device, platform.window),
|
|
MSAASamples = .SDL_GPU_SAMPLECOUNT_1,
|
|
SwapchainComposition = .SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
|
|
PresentMode = .SDL_GPU_PRESENTMODE_VSYNC,
|
|
};
|
|
|
|
success = ImplSDLGPU3_Init(*init_info);
|
|
ok_or_exit(success, "failed to init SDLGPU3 with error = %", consume_sdl_error());
|
|
}
|
|
#if USING_WINDOWS {
|
|
ImGui.ImplWin32_Init(hwnd);
|
|
ImGui.ImplDX11_Init(
|
|
cast,no_check(*ImGui.ID3D11Device) g_pd3dDevice,
|
|
cast,no_check(*ImGui.ID3D11DeviceContext) g_pd3dDeviceContext);
|
|
}
|
|
|
|
// #Startup:Font,Config,etc.
|
|
app_imgui_load_fonts();
|
|
app_imgui_config_setup();
|
|
app_apply_font_settings();
|
|
|
|
{ // #Startup:App
|
|
app_theme_startup(); // registry + settings.config + apply
|
|
if settings_write_back app_save_user_settings_file();
|
|
app_settings_watch_start();
|
|
app_ui_startup();
|
|
}
|
|
|
|
#if USING_WINDOWS || OS == .MACOS platform.main_context = *context;
|
|
|
|
// #MainLoop
|
|
while !app.should_exit_early {
|
|
defer reset_temporary_storage();
|
|
if !app_frame() break;
|
|
}
|
|
|
|
// ImGui otherwise writes the layout only on its periodic autosave, so recent changes were lost.
|
|
ini_filename := ImGui.GetIO().IniFilename;
|
|
if ini_filename ImGui.SaveIniSettingsToDisk(ini_filename);
|
|
// By the time the loop above exits, app_exit_shutdown_poll (called from app_frame) has already
|
|
// confirmed the background shutdown work finished.
|
|
if app.exit_code exit(app.exit_code); // e.g. a headless test run with failures
|
|
}
|
|
|
|
// Returns false when the main loop should exit.
|
|
app_frame :: (in_live_resize := false) -> bool {
|
|
using platform; using ImGui;
|
|
|
|
app.frame_time = current_time_monotonic();
|
|
|
|
#if COMPILE_TESTS testing_cleanup();
|
|
platform_update_memory_usage_metrics();
|
|
|
|
app.frame_count += 1;
|
|
|
|
// #shared_memory_channel:bringToFront
|
|
if app.is_master && app.read_channel_fatal_errors < 2 {
|
|
message_success, fatal_error, message := Shared_Channel.reader_poll_for_message(*app.read_channel);
|
|
|
|
if fatal_error {
|
|
app.read_channel_fatal_errors += 1;
|
|
if app.read_channel_fatal_errors == 1 {
|
|
log_error("[IPC] The shared memory channel is corrupted; discarding its queue.");
|
|
Shared_Channel.reader_reset_queue(*app.read_channel);
|
|
} else {
|
|
log_error("[IPC] The shared memory channel is corrupted again; no longer polling it. Later launches will not bring this window forward.");
|
|
}
|
|
}
|
|
|
|
if message_success {
|
|
assert(message.user_read_bytes > 0);
|
|
log("[IPC] message received: message[%] %", message.user_read_bytes, []u8.{message.user_read_bytes, message.user_read_pointer});
|
|
command := cast(App_Shared_Memory_Tag)message.user_read_pointer[0];
|
|
if command == .Bring_To_Front {
|
|
app.restore_window_this_frame = true;
|
|
}
|
|
Shared_Channel.reader_done_with_message(*message);
|
|
}
|
|
}
|
|
// #frameStart
|
|
#if USING_SDL posix_exit_signal_check();
|
|
// Once the background shutdown thread owns the settings watch (exit_shutdown.jai), the main
|
|
// thread must leave it alone to avoid racing it.
|
|
shutting_down := app_exit_shutdown_started();
|
|
if !shutting_down {
|
|
app_settings_watch_frame();
|
|
}
|
|
app_check_key_combinations();
|
|
app_ui_frame_start();
|
|
|
|
// On macOS a live-resize frame runs from an event watch inside SDL_PollEvent, so it must not poll.
|
|
#if USING_SDL if !in_live_resize {
|
|
#if OS == .MACOS {
|
|
platform.polling_events = true;
|
|
platform.live_resize_frame_drawn = false;
|
|
}
|
|
ev: SDL_Event;
|
|
while(SDL_PollEvent(*ev)) {
|
|
// Send events to imgui through the backend, then do our processing
|
|
event_consumed := ImplSDL3_ProcessEvent(*ev);
|
|
|
|
if ev.type.(SDL_EventType) == {
|
|
case SDL_EventType.SDL_EVENT_WINDOW_MINIMIZED;
|
|
app.minimize_to_tray_this_frame = true;
|
|
|
|
case SDL_EventType.QUIT;
|
|
app.should_exit = true;
|
|
|
|
case SDL_EventType.WINDOW_CLOSE_REQUESTED;
|
|
app.minimize_to_tray_this_frame = true;
|
|
|
|
case;
|
|
}
|
|
}
|
|
|
|
SDL_PumpEvents();
|
|
SDL_UpdateTrays();
|
|
#if OS == .MACOS {
|
|
platform.polling_events = false;
|
|
// A live-resize frame drew during the poll and released the temporary storage it rebuilt the
|
|
// per-frame state (anything app_ui_frame_start builds) in, so this frame must not draw with it.
|
|
if platform.live_resize_frame_drawn return true;
|
|
}
|
|
} // #if USING_SDL
|
|
|
|
// A live-resize frame runs from WM_PAINT inside the pump, so it must neither pump nor swap devices.
|
|
#if USING_WINDOWS {
|
|
if !in_live_resize {
|
|
// Also checks registered global hotkeys! The flags it sets wait for the next frame if this one ends here.
|
|
if !Windows_Read_Messages() return true;
|
|
|
|
// Between the message pump and NewFrame is the only safe place for these; see #WarpFirstStartup.
|
|
d3d11_swap_to_hardware_if_ready();
|
|
d3d11_recover_lost_device_if_needed();
|
|
}
|
|
win32_apply_dpi_change(); // also in a live-resize frame: dragging across monitors happens there
|
|
}
|
|
|
|
#if USING_SDL sdl_apply_pending_app_icon(); // a frame after the restore below asked for it
|
|
if app.minimize_to_tray_this_frame {
|
|
app.minimize_to_tray_this_frame = false;
|
|
platform_minimize_to_tray();
|
|
}
|
|
if app.restore_window_this_frame {
|
|
app.restore_window_this_frame = false;
|
|
app.last_window_restore_time = current_time_monotonic();
|
|
platform_restore_window();
|
|
}
|
|
|
|
if app.should_exit {
|
|
app_exit_shutdown_start();
|
|
if app_exit_shutdown_poll() return false;
|
|
}
|
|
|
|
// Whichever way the window was hidden or brought back (tray click, hotkey, a second launch).
|
|
imgui_show_floating_windows(!app.minimized_to_tray);
|
|
if app.minimized_to_tray != app.was_minimized_to_tray {
|
|
app.was_minimized_to_tray = app.minimized_to_tray;
|
|
imgui_drop_stale_input();
|
|
// The minimize hotkey's 0.15 s guard, for every way back.
|
|
if !app.minimized_to_tray app.last_window_restore_time = current_time_monotonic();
|
|
}
|
|
|
|
#if USING_WINDOWS {
|
|
// While hidden in the tray, a lost device waits as any hidden frame does, not every 16 ms.
|
|
if g_device_lost || !g_pSwapChain {
|
|
if app.minimized_to_tray platform_wait_while_hidden(HIDDEN_WAIT_MS); else sleep_milliseconds(16);
|
|
return true;
|
|
}
|
|
|
|
if g_SwapChainOccluded && IDXGISwapChain_Present(g_pSwapChain, 0, DXGI_PRESENT_TEST) == DXGI_STATUS_OCCLUDED {
|
|
platform_wait_while_hidden(HIDDEN_WAIT_MS);
|
|
return true;
|
|
}
|
|
g_SwapChainOccluded = false;
|
|
|
|
D3D11_HandleWindowResize();
|
|
if d3d11_device_was_removed() || !g_mainRenderTargetView {
|
|
if app.minimized_to_tray platform_wait_while_hidden(HIDDEN_WAIT_MS); else sleep_milliseconds(16);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// :InputDisabledOnMinimize: no #UI block, so no input handling, runs while hidden.
|
|
if app.minimized_to_tray {
|
|
platform_wait_while_hidden(HIDDEN_WAIT_MS);
|
|
return true;
|
|
}
|
|
|
|
imgui_reset_layout_if_requested();
|
|
|
|
// #ImGui:NewFrame
|
|
#if USING_SDL {
|
|
ImplSDLGPU3_NewFrame();
|
|
ImplSDL3_NewFrame();
|
|
}
|
|
#if USING_WINDOWS {
|
|
ImGui.ImplDX11_NewFrame();
|
|
ImGui.ImplWin32_NewFrame();
|
|
}
|
|
ImGui.NewFrame();
|
|
|
|
|
|
{ // #UI Start
|
|
ImGui.DockSpaceOverViewport();
|
|
|
|
app_debug_panel_ui();
|
|
app_frame_times_ui();
|
|
app_style_editor_ui();
|
|
app_logger_panel_ui();
|
|
|
|
app_main_window_ui(); // app/app_ui.jai: start here
|
|
|
|
app_toast_ui();
|
|
app_exit_shutdown_overlay_ui(); // last, so it draws over everything else
|
|
} // #UI End
|
|
|
|
// #ImGui:Rendering
|
|
ImGui.Render();
|
|
|
|
swapchain_wait: Apollo_Time; // left out of the recorded work time
|
|
#if USING_SDL {
|
|
draw_data := GetDrawData();
|
|
is_minimized := (draw_data.DisplaySize.x <= 0 || draw_data.DisplaySize.y <= 0);
|
|
|
|
// Acquire a GPU command buffer and swapchain texture. With vsync these can block until the
|
|
// display frees an image (Metal's nextDrawable), which is the vsync wait, not work.
|
|
acquire_start := current_time_monotonic();
|
|
cmd_buffer := SDL_AcquireGPUCommandBuffer(gpu_device);
|
|
|
|
swapchain_texture: *SDL_GPUTexture;
|
|
success := SDL_AcquireGPUSwapchainTexture(cmd_buffer, platform.window, *swapchain_texture, null, null);
|
|
swapchain_wait = current_time_monotonic() - acquire_start;
|
|
|
|
if swapchain_texture && !is_minimized {
|
|
|
|
ImplSDLGPU3_PrepareDrawData(draw_data, cmd_buffer);
|
|
|
|
target_info := SDL_GPUColorTargetInfo.{
|
|
texture = swapchain_texture,
|
|
clear_color = app.ui_global_clear_color,
|
|
load_op = .CLEAR,
|
|
store_op = .STORE,
|
|
mip_level = 0,
|
|
layer_or_depth_plane = 0,
|
|
cycle = false,
|
|
};
|
|
|
|
render_pass := SDL_BeginGPURenderPass(cmd_buffer, *target_info, 1, null);
|
|
|
|
ImplSDLGPU3_RenderDrawData(draw_data, cmd_buffer, render_pass);
|
|
|
|
SDL_EndGPURenderPass(render_pass);
|
|
} else {
|
|
// Minimized to the Dock (no tray), there is no swapchain texture to wait on for vsync, so
|
|
// without this the loop would spin.
|
|
platform_wait_while_hidden(HIDDEN_WAIT_MS);
|
|
}
|
|
} // #if USING_SDL
|
|
// The frame is still ended, with UpdatePlatformWindows, but not drawn or presented.
|
|
draw_frame := true;
|
|
#if USING_WINDOWS {
|
|
draw_frame = !d3d11_device_was_removed();
|
|
if draw_frame {
|
|
ID3D11DeviceContext_OMSetRenderTargets(g_pd3dDeviceContext, 1, *g_mainRenderTargetView, null);
|
|
ID3D11DeviceContext_ClearRenderTargetView(g_pd3dDeviceContext, g_mainRenderTargetView, *app.ui_global_clear_color);
|
|
|
|
ImGui.ImplDX11_RenderDrawData(ImGui.GetDrawData());
|
|
}
|
|
}
|
|
|
|
io := ImGui.GetIO();
|
|
|
|
// #ImGui: Update and Render additional Platform Windows
|
|
if io.ConfigFlags_ & .ViewportsEnable {
|
|
// ImGui requires this every frame, but on a removed device the DX11 backend asserts creating a
|
|
// swap chain for a new floating window. None is made then: recovery recreates every window.
|
|
#if USING_WINDOWS {
|
|
platform_io := ImGui.GetPlatformIO();
|
|
renderer_create_window := platform_io.Renderer_CreateWindow;
|
|
if !draw_frame platform_io.Renderer_CreateWindow = null;
|
|
ImGui.UpdatePlatformWindows();
|
|
platform_io.Renderer_CreateWindow = renderer_create_window;
|
|
} else {
|
|
ImGui.UpdatePlatformWindows();
|
|
}
|
|
if draw_frame ImGui.RenderPlatformWindowsDefault();
|
|
}
|
|
|
|
work_seconds := to_float64_seconds(current_time_monotonic() - app.frame_time - swapchain_wait);
|
|
insert(*perf.frame_times, Frame_Timing.{work = xx work_seconds, delta = ImGui.GetIO().DeltaTime});
|
|
|
|
// #ImGui:Present and VSYNC
|
|
#if USING_SDL {
|
|
SDL_SubmitGPUCommandBuffer(cmd_buffer);
|
|
}
|
|
#if USING_WINDOWS {
|
|
if draw_frame DXGI_Handle_Vsync();
|
|
}
|
|
|
|
if perf.time_to_first_frame_ms == 0 {
|
|
perf.time_to_first_frame_ms = to_float64_seconds(app.frame_time - app.startup_time) * 1000.0;
|
|
}
|
|
|
|
if (perf.backend == .Vulkan || app.sleep_on_fast_frame) && (ImGui.GetIO().DeltaTime < QUICK_FRAME_DT) {
|
|
// Sleep for 1 ms after a quick frame to avoid pegging the core
|
|
sleep_milliseconds(1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QUICK_FRAME_DT : float64 : 1.0 / 300.0;
|
|
|
|
// The IPC channel is only polled, so a hidden window still wakes this often to notice a
|
|
// Bring_To_Front from a second launch; window messages and SDL events end the wait sooner.
|
|
HIDDEN_WAIT_MS :: 50;
|
|
|
|
// POSIX shared memory names are machine-wide, so the name carries the user id; otherwise a
|
|
// second user's launch would find the first user's channel and exit.
|
|
app_shared_memory_channel_key :: () -> string {
|
|
#if OS == .MACOS {
|
|
// macOS caps the name at 31 characters (PSHMNAMLEN), which a long folder name plus a user id
|
|
// of six or more digits, as directory and mobile accounts have, would pass.
|
|
MAX_NAME_PREFIX :: 16;
|
|
prefix := APP_FOLDER_NAME;
|
|
if prefix.count > MAX_NAME_PREFIX prefix.count = MAX_NAME_PREFIX;
|
|
return sprint("%_%", prefix, POSIX.getuid());
|
|
} else #if USING_SDL {
|
|
return sprint("%_ipc_%", APP_FOLDER_NAME, POSIX.getuid());
|
|
} else {
|
|
return sprint("%_shared_memory_channel", APP_FOLDER_NAME);
|
|
}
|
|
}
|
|
|
|
#if USING_SDL {
|
|
// Per-user, so another user can neither plant a symlink at it nor hold it against this one.
|
|
app_instance_lock_path :: () -> string {
|
|
#if OS == .MACOS {
|
|
directory := to_string(SDL_getenv("TMPDIR")); // a per-user directory on macOS
|
|
} else {
|
|
directory := to_string(SDL_getenv("XDG_RUNTIME_DIR"));
|
|
if !directory {
|
|
// Unset under sudo, some ssh sessions, and non-systemd sessions.
|
|
home := to_string(SDL_getenv("HOME"));
|
|
if !home return "";
|
|
directory = tprint("%/.cache", home);
|
|
make_directory_if_it_does_not_exist(directory);
|
|
}
|
|
}
|
|
if !directory return "";
|
|
|
|
return tprint("%/%.lock", trim_right(directory, "/"), APP_FOLDER_NAME);
|
|
}
|
|
}
|
|
|
|
// Returns true once connected to a running instance, whether or not the message got through.
|
|
app_bring_running_instance_to_front :: (channel_key: string, attempts: int) -> bool {
|
|
connected := false;
|
|
for 1..attempts {
|
|
connected, app.write_channel = Shared_Channel.writer_connect(channel_key, name_not_found_is_quiet=true);
|
|
if connected break;
|
|
if it < attempts sleep_milliseconds(50);
|
|
}
|
|
if !connected return false;
|
|
defer Shared_Channel.writer_disconnect(*app.write_channel);
|
|
|
|
// Reserving fails when the queue is full, e.g. the running instance has stopped polling.
|
|
message_success, message := Shared_Channel.writer_reserve_message(*app.write_channel, APP_SHARED_MEMORY_CHANNEL_HEADER_SIZE);
|
|
if !message_success {
|
|
log_error("Another instance is running, but no space could be reserved in its message queue; it was not brought to the front.");
|
|
return true;
|
|
}
|
|
message.user_write_pointer[0] = cast(u8)App_Shared_Memory_Tag.Bring_To_Front;
|
|
message.user_write_pointer[1] = 0;
|
|
|
|
#if USING_WINDOWS AllowSetForegroundWindow(ASFW_ANY);
|
|
|
|
if !Shared_Channel.writer_commit_message(*app.write_channel, message) {
|
|
log_error("Another instance is running, but the bring-to-front message could not be committed.");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
panic :: (format_string: string, args: .. Any, exit_code : s32 = 1) {
|
|
log_error(format_string, ..args);
|
|
exit(exit_code);
|
|
} @PrintLike
|