ui2 induced sokol gg improvements (#31051)

* parser, sokol.sapp: embed files on android, add the NativeActivity entry

parser:  only embedded its bytes for -prod, -os cross and
builds of V itself. Every other build re-read the file at runtime from
the compile-time paths (path/apath), which on android names locations on
the machine that compiled the app - the reload panic aborted the app at
startup. Embed on android like a portable cross snapshot does.

sokol.sapp: sokol_app.h's Android glue calls sokol_main() to get the
app's sapp_desc, and nothing on the V side provided it: V generates the
program entry as plain main(), the v1 compiler generated sokol_main only
with -apk, and the v3 driver accepts -apk as a no-op. Every android
sokol app died in dlopen with 'cannot locate symbol sokol_main'. The new
sokol_main_android glue runs main() and returns the sapp__g_desc the
module's sapp.run() recorded, which is the contract sapp_run's empty
stub implies.

* sokol.sapp: let X11 apps skip presenting frames they did not draw

sapp_set_present_on_demand(true) makes the X11 loop (EGL and GLX) present a
frame only when its frame callback called sapp_request_present(). The mode
is off by default, so existing apps keep presenting every frame. Backends
that cannot honor it, such as Wayland, Windows and macOS, return false and
keep presenting. UI2 uses it on Linux to stop repainting idle windows.

* modify thirdparty/sokol/sokol_app.h
This commit is contained in:
diviocity authored and GitHub committed 2026-10-26 05:30:28 +03:00
1 parent f91bd9d6b7
commit 579f1e6044
4 files changed
+193 -8

No files matched your search

+146 -7
View File
@@ -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 <dlfcn.h> /* dlopen, dlsym, dlclose */
#include <fcntl.h> /* fcntl, F_GETFL, F_SETFL, O_NONBLOCK for the wakeup pipe */
#include <limits.h> /* LONG_MAX */
#include <poll.h>
#include <pthread.h> /* 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; }
+23
View File
@@ -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;
}
+15
View File
@@ -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"
}
+9 -1
View File
@@ -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) {