diff --git a/thirdparty/sokol/sokol_app.h b/thirdparty/sokol/sokol_app.h index 9a15816ccc..dc97eaae37 100644 --- a/thirdparty/sokol/sokol_app.h +++ b/thirdparty/sokol/sokol_app.h @@ -2399,6 +2399,23 @@ SOKOL_APP_API_DECL void sapp_request_quit(void); /* cancel a pending quit (when SAPP_EVENTTYPE_QUIT_REQUESTED has been received) */ SOKOL_APP_API_DECL void sapp_cancel_quit(void); +/* X11 (EGL or GLX) only: when enabled, a frame whose callback did not call + sapp_request_present() is not presented. Returns false where the backend + cannot honor it, which leaves the mode off. */ +SOKOL_APP_API_DECL bool sapp_set_present_on_demand(bool enabled); +/* X11 only: present the frame drawn by the current frame callback */ +SOKOL_APP_API_DECL void sapp_request_present(void); +/* X11 only: deadline-based idle polling. Registering a callback switches the + event loop from pacing itself at the swap interval to sleeping until the + earliest of: the callback's returned deadline (seconds from now, negative + when nothing is scheduled), an X event, or a call to sapp_wake(). The + callback runs on the frame thread just before the loop sleeps, so it may + read shared state without taking the app's own locks. */ +SOKOL_APP_API_DECL void sapp_set_frame_deadline_cb(double (*cb)(void)); +/* X11 only: interrupt the event loop's current sleep from any thread. Safe to + call before the loop starts and after it ends; a request arriving while the + loop is awake is folded into the next frame. */ +SOKOL_APP_API_DECL void sapp_wake(void); /* initiate a "hard quit" (quit application without sending * SAPP_EVENTTYPE_QUIT_REQUESTED) */ SOKOL_APP_API_DECL void sapp_quit(void); @@ -2850,6 +2867,7 @@ extern const struct wl_interface zxdg_decoration_manager_v1_interface; #include "xdg-shell-client-protocol.h" #endif #include /* dlopen, dlsym, dlclose */ +#include /* fcntl, F_GETFL, F_SETFL, O_NONBLOCK for the wakeup pipe */ #include /* LONG_MAX */ #include #include /* only used a linker-guard, search for _sapp_linux_run() and see first comment */ @@ -3551,6 +3569,12 @@ typedef struct { Atom NET_WM_STATE_FULLSCREEN; _sapp_xi_t xi; _sapp_xdnd_t xdnd; + // Wakeup pipe for cross-thread frame requests. Any thread that needs a frame + // before the loop's current sleep horizon expires writes to this pipe so the + // loop's poll interrupts immediately instead of waiting out the timeout. + int wakeup_fd[2]; + // Deadline callback for deadline-based idle polling (sapp_set_frame_deadline_cb). + double (*frame_deadline_cb)(void); // XLib manual says keycodes are in the range [8, 255] inclusive. // https://tronche.com/gui/x/xlib/input/keyboard-encoding.html bool key_repeat[_SAPP_X11_MAX_X11_KEYCODES]; @@ -3740,6 +3764,8 @@ typedef struct { bool cleanup_called; bool quit_requested; bool quit_ordered; + bool present_on_demand; + bool present_requested; bool event_consumed; bool html5_ask_leave_site; bool onscreen_keyboard_shown; @@ -15175,11 +15201,14 @@ _SOKOL_PRIVATE void _sapp_linux_frame(void) { _sapp_vk_frame(); #else _sapp_frame(); + if (!_sapp.present_on_demand || _sapp.present_requested) { #if defined(_SAPP_GLX) - _sapp_glx_swap_buffers(); + _sapp_glx_swap_buffers(); #elif defined(_SAPP_EGL) - eglSwapBuffers(_sapp.egl.display, _sapp.egl.surface); + eglSwapBuffers(_sapp.egl.display, _sapp.egl.surface); #endif + } + _sapp.present_requested = false; #endif } @@ -15194,6 +15223,11 @@ _SOKOL_PRIVATE void _sapp_linux_run(const sapp_desc *desc) { _sapp_init_state(desc); _sapp.x11.window_state = NormalState; + // The wakeup pipe starts closed: any thread calling sapp_wake() before or + // after the loop runs must never write to stdin/stdout by accident. + _sapp.x11.wakeup_fd[0] = -1; + _sapp.x11.wakeup_fd[1] = -1; + _sapp.x11.frame_deadline_cb = 0; XInitThreads(); XrmInitialize(); @@ -15201,6 +15235,22 @@ _SOKOL_PRIVATE void _sapp_linux_run(const sapp_desc *desc) { if (!_sapp.x11.display) { _SAPP_PANIC(LINUX_X11_OPEN_DISPLAY_FAILED); } + if (pipe(_sapp.x11.wakeup_fd) == 0) { + // Both ends nonblocking: the loop's poll reports readability without + // blocking on a partial drain, and sapp_wake() from any thread never + // stalls on a full pipe. + for (int i = 0; i < 2; i++) { + int flags = fcntl(_sapp.x11.wakeup_fd[i], F_GETFL, 0); + if (flags >= 0) { + fcntl(_sapp.x11.wakeup_fd[i], F_SETFL, flags | O_NONBLOCK); + } + } + } else { + // No wakeup pipe: the loop falls back to its stock frame pacing instead + // of deadline sleeps, so losing the pipe degrades rather than breaks. + _sapp.x11.wakeup_fd[0] = -1; + _sapp.x11.wakeup_fd[1] = -1; + } _sapp.x11.screen = DefaultScreen(_sapp.x11.display); _sapp.x11.root = DefaultRootWindow(_sapp.x11.display); _sapp_x11_query_system_dpi(); @@ -15261,11 +15311,53 @@ _SOKOL_PRIVATE void _sapp_linux_run(const sapp_desc *desc) { clock_gettime(_SAPP_CLOCK_MONOTONIC, &frame_end_ts); const long elapsed_ms = (frame_end_ts.tv_sec - frame_start_ts.tv_sec) * 1000L + (frame_end_ts.tv_nsec - frame_start_ts.tv_nsec) / 1000000L; - const long frame_ms = (long)((1000.0 / 60.0) * _sapp.swap_interval); - const long remaining_ms = frame_ms - elapsed_ms; - if (remaining_ms > 0) { - struct pollfd x11_fd = { ConnectionNumber(_sapp.x11.display), POLLIN, 0 }; - poll(&x11_fd, 1, (int)remaining_ms); + /* Decide how long to sleep. With a deadline callback the loop sleeps + until the earliest of the returned deadline, an X event, or a + sapp_wake(); a negative return means nothing is scheduled and the + loop waits on the poll set alone, which is what drops idle wakeups + from once per swap interval to once per actual ask. Without a + callback the stock frame pacing is kept so existing callers are + unchanged. */ + long timeout_ms; + if (_sapp.x11.frame_deadline_cb) { + const double s = _sapp.x11.frame_deadline_cb(); + if (s < 0.0) { + timeout_ms = -1; + } else { + timeout_ms = (long)(s * 1000.0); + if (timeout_ms < 0) { + timeout_ms = 0; + } + } + } else { + const long frame_ms = (long)((1000.0 / 60.0) * _sapp.swap_interval); + timeout_ms = frame_ms - elapsed_ms; + if (timeout_ms < 0) { + timeout_ms = 0; + } + } + struct pollfd fds[2]; + int nfds = 0; + fds[nfds].fd = ConnectionNumber(_sapp.x11.display); + fds[nfds].events = POLLIN; + fds[nfds].revents = 0; + nfds++; + if (_sapp.x11.wakeup_fd[0] >= 0) { + fds[nfds].fd = _sapp.x11.wakeup_fd[0]; + fds[nfds].events = POLLIN; + fds[nfds].revents = 0; + nfds++; + } + poll(fds, nfds, (int)timeout_ms); + // Drain the wakeup pipe unconditionally: a byte left inside would make + // the next poll return immediately and turn the sleep into a spin, + // even when an X event woke this pass first. The read is nonblocking + // and returns immediately when the pipe is empty. + if (_sapp.x11.wakeup_fd[0] >= 0) { + char drain_buf[256]; + while (read(_sapp.x11.wakeup_fd[0], drain_buf, sizeof(drain_buf)) > 0) { + // discard + } } } // handle quit-requested, either from window or from sapp_request_quit() @@ -15291,6 +15383,14 @@ _SOKOL_PRIVATE void _sapp_linux_run(const sapp_desc *desc) { _sapp_x11_destroy_window(); _sapp_x11_destroy_standard_cursors(); XCloseDisplay(_sapp.x11.display); + if (_sapp.x11.wakeup_fd[0] >= 0) { + close(_sapp.x11.wakeup_fd[0]); + _sapp.x11.wakeup_fd[0] = -1; + } + if (_sapp.x11.wakeup_fd[1] >= 0) { + close(_sapp.x11.wakeup_fd[1]); + _sapp.x11.wakeup_fd[1] = -1; + } _sapp_discard_state(); } @@ -17156,6 +17256,45 @@ SOKOL_API_IMPL void sapp_request_quit(void) { _sapp.quit_requested = true; } SOKOL_API_IMPL void sapp_cancel_quit(void) { _sapp.quit_requested = false; } +SOKOL_API_IMPL bool sapp_set_present_on_demand(bool enabled) { +#if defined(_SAPP_LINUX) && defined(_SAPP_X11) && !defined(SOKOL_WGPU) && !defined(SOKOL_VULKAN) + _sapp.present_on_demand = enabled; + return true; +#else + _sapp.present_on_demand = false; + return false; +#endif +} + +SOKOL_API_IMPL void sapp_request_present(void) { _sapp.present_requested = true; } + +/* X11 only: deadline-based idle polling support. The callback runs on the + frame thread right before the loop sleeps, so it may read state without + taking application locks; its negative return means "sleep until an X event + or a sapp_wake()". */ +SOKOL_API_IMPL void sapp_set_frame_deadline_cb(double (*cb)(void)) { +#if defined(_SAPP_LINUX) && defined(_SAPP_X11) + _sapp.x11.frame_deadline_cb = cb; +#else + (void)cb; +#endif +} + +SOKOL_API_IMPL void sapp_wake(void) { +#if defined(_SAPP_LINUX) && defined(_SAPP_X11) + const int wr = _sapp.x11.wakeup_fd[1]; + if (wr >= 0) { + const char b = 1; + // Nonblocking write: a full pipe means the loop has as many pending wake + // requests queued as it can hold, which is still one wake more than it + // needs to interrupt its sleep. + (void)write(wr, &b, 1); + } +#else + (void)0; +#endif +} + SOKOL_API_IMPL void sapp_quit(void) { _sapp.quit_ordered = true; } SOKOL_API_IMPL void sapp_consume_event(void) { _sapp.event_consumed = true; } diff --git a/vlib/sokol/sapp/sokol_main_android.c b/vlib/sokol/sapp/sokol_main_android.c new file mode 100644 index 0000000000..179dcf58fd --- /dev/null +++ b/vlib/sokol/sapp/sokol_main_android.c @@ -0,0 +1,23 @@ +// The Android NativeActivity entry point required by sokol_app.h. +// +// V's compiler generates the program's entry as a plain `main()`, and the +// user's code registers the app description through `sapp.run()`, which +// stores a copy in the `sapp__g_desc` global (while `sapp_run()` is an empty +// stub in sokol_app.h's normal entry mode). sokol_app.h's Android glue +// instead requires the program to provide `sokol_main()`, so this glue runs +// the program's `main()` and hands the registered description back to +// sokol_app.h, which then takes over the event loop. +// +// This file is textual C, included into the program's translation unit by +// sokol_main_android.c.v, and only for Android builds. +#include "sokol_app.h" + +extern int main(int argc, char *argv[]); +extern sapp_desc sapp__g_desc; + +sapp_desc sokol_main(int argc, char *argv[]) { + (void)argc; + (void)argv; + main(argc, argv); + return sapp__g_desc; +} diff --git a/vlib/sokol/sapp/sokol_main_android.c.v b/vlib/sokol/sapp/sokol_main_android.c.v new file mode 100644 index 0000000000..ebaf240764 --- /dev/null +++ b/vlib/sokol/sapp/sokol_main_android.c.v @@ -0,0 +1,15 @@ +// Copyright(C) 2026 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// file that is distributed with this source code. +module sapp + +// Provides the Android NativeActivity entry point required by sokol_app.h. +// See sokol_main_android.c for details. sokol_app.h itself is included by +// `sokol.c` when the program does not define `no_sokol_app`, which is also +// the condition for this glue: without the header there is no Android glue +// and no `sapp_desc` type to return. +$if android && !no_sokol_app ? { + #flag -I@VEXEROOT/vlib/sokol/sapp + @[use_once] + #include "sokol_main_android.c" +} diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index be56d18591..59a5ee2739 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -8611,7 +8611,15 @@ fn (mut p Parser) parse_embed_file_expr() flat.NodeId { // copies into every program, and its source tree changes under it: after a `git pull`, // the executable that has to rebuild V would otherwise generate C from the new headers. fn (mut p Parser) embed_file_uncompressed_data(apath string) ?flat.NodeId { - if !p.prefs.is_prod && !p.prefs.output_cross_c && !p.prefs.building_v { + mut embed := p.prefs.is_prod || p.prefs.output_cross_c || p.prefs.building_v + if p.prefs.target.os == 'android' { + // An Android app cannot read the compile-time file system: `path` and + // `apath` name locations on the machine that compiled the program, and + // the panic in `reload_from_file_at_runtime` would abort the app at + // startup. Embed the bytes, like a portable `-os cross` snapshot does. + embed = true + } + if !embed { return none } if apath.len == 0 || !os.is_file(apath) {