From e586f29fe9e28728e4b9d2ba8d89b631d625171a Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:47:45 +0200 Subject: [PATCH 1/5] Embedded GPU: do not tear down EGL at exit; Savor: log why a live channel ends The nightly embedded-linux GPU run segfaulted after the demo finished: exit() ran cn1GpuShutdown from the atexit hook, which terminated EGL and freed the glyph atlas while the EDT was still uploading it. At exit only the screen state is put back now; the kernel frees the rest. Savor's LiveSocketTest failed once with the server closing the channel between two pushes, and nothing said why. LiveHub logs a send that fails before aborting, LiveEndpoint logs an abnormal close of an admitted connection, and the test says how far it got. Co-Authored-By: Claude Opus 5.5 --- .../nativeSources/cn1_linux_embedded.c | 2 +- Ports/LinuxPort/nativeSources/cn1_linux_gpu.c | 16 ++++++++++++++++ Ports/LinuxPort/nativeSources/cn1_linux_gpu.h | 5 +++++ .../examples/savor/live/LiveEndpoint.java | 8 ++++++++ .../examples/savor/live/LiveHub.java | 18 ++++++++++++++++++ .../examples/savor/LiveSocketTest.java | 9 ++++++++- 6 files changed, 56 insertions(+), 2 deletions(-) diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c b/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c index 2aeca1aa06a..cc40cbae2b0 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c @@ -711,7 +711,7 @@ static void cn1Present(CN1EmbRect damage) { static void cn1RestoreScreen(void) { #ifdef CN1_EMBEDDED_GPU - cn1GpuShutdown(); + cn1GpuShutdownAtExit(); #endif #ifdef CN1_EMBEDDED_HAS_DRM if (cn1DrmFd >= 0) { diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_gpu.c b/Ports/LinuxPort/nativeSources/cn1_linux_gpu.c index 05b503856bb..193a48c1dff 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_gpu.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_gpu.c @@ -1155,4 +1155,20 @@ void cn1GpuShutdown(void) { cn1gTearDown(); } +void cn1GpuShutdownAtExit(void) { + /* Runs from exit() on whichever thread called System.exit, while the EDT + * may still be in the middle of a frame. Tearing the context down here + * freed the glyph atlas and terminated EGL under a texture upload in + * flight -- a segfault after the application had already finished. The + * process is going away, so its buffers are released by the kernel; only + * the screen state outlives it and needs putting back. */ + if (!cn1gActive) { + return; + } + if (cn1gScanout) { + (void) cn1gWaitFlip(); + cn1GpuCursor(0, 0, 0); + } +} + #endif /* CN1_LINUX_EMBEDDED && CN1_EMBEDDED_GPU */ diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_gpu.h b/Ports/LinuxPort/nativeSources/cn1_linux_gpu.h index 4c8a0c5483b..080779bbc3b 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_gpu.h +++ b/Ports/LinuxPort/nativeSources/cn1_linux_gpu.h @@ -103,6 +103,11 @@ int cn1GpuCursor(int show, int px, int py); * puts the console's CRTC back. */ void cn1GpuShutdown(void); +/* cn1GpuShutdown for the atexit path: hides the cursor plane and waits out a + * pending flip, but releases nothing, because another thread may still be + * drawing with the context. */ +void cn1GpuShutdownAtExit(void); + #ifdef __cplusplus } #endif diff --git a/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveEndpoint.java b/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveEndpoint.java index 196a992e105..ae602f3a5d1 100644 --- a/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveEndpoint.java +++ b/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveEndpoint.java @@ -90,6 +90,14 @@ public void onBinary(WebSocketSession session, byte[] message, int offset, int l @Override public void onClose(WebSocketSession session, int code, String reason) { Object username = session.getAttachment(); + // Only an admitted connection; a refused one has no user and its end is + // expected. 1000 and 1001 are the app leaving. Anything else -- 1006 + // above all, a connection lost with no Close -- is what to read first + // when pushes stop arriving, so it costs a line per dropped app. + if (username != null && code != 1000 && code != 1001) { + System.err.println("[live] websocket " + session.getId() + " of " + username + + " closed " + code + (reason == null || reason.isEmpty() ? "" : " " + reason)); + } if (username instanceof String) { hub.remove((String) username, session); } diff --git a/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveHub.java b/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveHub.java index a9e6f43657c..1e3cf6e90b5 100644 --- a/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveHub.java +++ b/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveHub.java @@ -198,12 +198,30 @@ private void collect(List to, String username) { /// Sends outside the lock: a slow client holds up its own frame, not /// everyone's. One that cannot be written to is dropped; its close callback /// takes it out of the maps. + /// The `type` of a frame this class wrote, for a log line; never the + /// payload, which carries positions and order details. + static String frameType(String text) { + int at = text.indexOf("\"type\":\""); + if (at < 0) { + return "a frame"; + } + at += 8; + int end = text.indexOf('"', at); + return end < 0 ? "a frame" : "a " + text.substring(at, end) + " frame"; + } + private static void send(List to, String text) { for (int iter = 0; iter < to.size(); iter++) { WebSocketSession session = to.get(iter); try { session.sendText(text); } catch (IOException gone) { + // Said out loud: a dropped channel is otherwise invisible -- the + // app just stops hearing about its order, and a test sees only + // the connection end with no frame. + System.err.println("[live] websocket " + session.getId() + " of " + + session.getAttachment() + " dropped; could not send " + + frameType(text) + ": " + gone); session.abort(); } } diff --git a/scripts/savor/backend/src/test/java/com/codenameone/examples/savor/LiveSocketTest.java b/scripts/savor/backend/src/test/java/com/codenameone/examples/savor/LiveSocketTest.java index 52c82e6d185..0d2154d18d4 100644 --- a/scripts/savor/backend/src/test/java/com/codenameone/examples/savor/LiveSocketTest.java +++ b/scripts/savor/backend/src/test/java/com/codenameone/examples/savor/LiveSocketTest.java @@ -59,6 +59,8 @@ class LiveSocketTest { /// The last frame read: its opcode, and its payload as text. private int opcode; private byte[] payload; + /// Frames read so far in this test, for the message when one never comes. + private int framesRead; @Test void aTicketOpensTheChannelOnceAndPushesArrive() throws Exception { @@ -192,7 +194,11 @@ private int closeCode() { private void readFrame(Tcp connection) throws IOException { int first = readByte(connection); if (first < 0) { - throw new IOException("the connection ended with no frame"); + // The server closed it. Why is in its own log, the "[live]" lines + // above this failure; say where the test had got to. + throw new IOException("the connection ended with no frame; " + framesRead + + " frames read in this test, the last opcode " + opcode + ": " + + (payload == null ? "none" : new String(payload, "UTF-8"))); } opcode = first & 0xf; int length = readByte(connection) & 0x7f; @@ -208,6 +214,7 @@ private void readFrame(Tcp connection) throws IOException { } got += read; } + framesRead++; } /// A client's frames are masked; a server refuses one that is not. From a8c86743853e07b296e4040075104268bcbb4475 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:50:37 +0200 Subject: [PATCH 2/5] Review: no flip wait at exit, no page flip after the console is restored, log websocket errors Co-Authored-By: Claude Opus 5.5 --- .../nativeSources/cn1_linux_embedded.c | 15 +++++++++++- Ports/LinuxPort/nativeSources/cn1_linux_gpu.c | 16 +++++++++---- Ports/LinuxPort/nativeSources/cn1_linux_gpu.h | 9 ++++--- .../examples/savor/live/LiveEndpoint.java | 9 +++++++ .../examples/savor/live/LiveHub.java | 24 +++++++++---------- 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c b/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c index cc40cbae2b0..0e62224d4e5 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c @@ -80,6 +80,7 @@ #include #include #include +#include #include #include #include @@ -133,6 +134,17 @@ static int cn1Dpi = 0; static int cn1Initialized = 0; static int cn1HeadlessForced = 0; /* enableHeadlessScreenshot was called */ static volatile int cn1Running = 1; +/* Set by the atexit hook before it puts the console's CRTC back. The EDT keeps + * running until the process ends and must not flip its own buffer back on top + * of the restored console; both present paths read this before a page flip. + * A flip the EDT already decided on can still land, so this narrows the + * window rather than closing it -- closing it would mean the exiting thread + * waiting on the EDT, which may be the thread that called exit. */ +static atomic_int cn1Exiting = 0; + +int cn1EmbeddedExiting(void) { + return atomic_load(&cn1Exiting); +} static volatile int cn1CurrentModifiers = 0; static int cn1HasTouch = 0, cn1HasPointer = 0; @@ -695,7 +707,7 @@ static void cn1Present(CN1EmbRect damage) { cn1DrawCursor(b, cn1CursorX, cn1CursorY); } #ifdef CN1_EMBEDDED_HAS_DRM - if (cn1Output == OUT_KMS) { + if (cn1Output == OUT_KMS && !cn1EmbeddedExiting()) { if (drmModePageFlip(cn1DrmFd, cn1DrmCrtc, b->fbId, DRM_MODE_PAGE_FLIP_EVENT, 0) == 0) { cn1FlipPending = 1; cn1Front = target; @@ -1188,6 +1200,7 @@ static void cn1StartInput(void) { /* ------------------------------------------------------------- natives */ static void cn1AtExit(void) { + atomic_store(&cn1Exiting, 1); cn1Running = 0; cn1RestoreScreen(); } diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_gpu.c b/Ports/LinuxPort/nativeSources/cn1_linux_gpu.c index 193a48c1dff..e468fde7be4 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_gpu.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_gpu.c @@ -1039,6 +1039,10 @@ void cn1GpuPresent(void) { if (!eglSwapBuffers(cn1gDpy, cn1gSurface)) { return; } + if (cn1EmbeddedExiting()) { + /* The console's CRTC is being put back; see cn1EmbeddedExiting. */ + return; + } bo = gbm_surface_lock_front_buffer(cn1gGbmSurface); if (bo == 0) { return; @@ -1162,11 +1166,13 @@ void cn1GpuShutdownAtExit(void) { * flight -- a segfault after the application had already finished. The * process is going away, so its buffers are released by the kernel; only * the screen state outlives it and needs putting back. */ - if (!cn1gActive) { - return; - } - if (cn1gScanout) { - (void) cn1gWaitFlip(); + /* Not even waiting out a pending flip: that runs the DRM event handler and + * swaps and releases the front and next buffers, which the EDT may be doing + * at this same moment in its own present -- the same race in a smaller + * window. Hiding the cursor plane is a single ioctl and touches no shared + * state; the display unit's drmModeSetCrtc then replaces the scanout + * whatever flip is still in flight. */ + if (cn1gActive && cn1gScanout) { cn1GpuCursor(0, 0, 0); } } diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_gpu.h b/Ports/LinuxPort/nativeSources/cn1_linux_gpu.h index 080779bbc3b..fc787f3fcf9 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_gpu.h +++ b/Ports/LinuxPort/nativeSources/cn1_linux_gpu.h @@ -103,11 +103,14 @@ int cn1GpuCursor(int show, int px, int py); * puts the console's CRTC back. */ void cn1GpuShutdown(void); -/* cn1GpuShutdown for the atexit path: hides the cursor plane and waits out a - * pending flip, but releases nothing, because another thread may still be - * drawing with the context. */ +/* cn1GpuShutdown for the atexit path: hides the cursor plane and releases + * nothing, because another thread may still be drawing with the context. */ void cn1GpuShutdownAtExit(void); +/* Nonzero once the process is exiting; the GPU stops page flipping then. + * Defined by the display unit, cn1_linux_embedded.c. */ +int cn1EmbeddedExiting(void); + #ifdef __cplusplus } #endif diff --git a/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveEndpoint.java b/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveEndpoint.java index ae602f3a5d1..a782f38d2d3 100644 --- a/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveEndpoint.java +++ b/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveEndpoint.java @@ -103,6 +103,15 @@ public void onClose(WebSocketSession session, int code, String reason) { } } + @Override + public void onError(WebSocketSession session, Exception error) { + // A read failure, a protocol error or a callback that threw: the backend + // reports it here and then closes, and without this line the only trace + // of why a channel ended would be the 1006 that onClose prints. + System.err.println("[live] websocket " + session.getId() + " of " + + session.getAttachment() + " failed: " + error); + } + /// The `ticket` parameter of a query string, or null. A ticket is hex, so /// there is nothing in it to decode. static String ticket(String query) { diff --git a/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveHub.java b/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveHub.java index 1e3cf6e90b5..ebe54108f83 100644 --- a/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveHub.java +++ b/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveHub.java @@ -198,18 +198,6 @@ private void collect(List to, String username) { /// Sends outside the lock: a slow client holds up its own frame, not /// everyone's. One that cannot be written to is dropped; its close callback /// takes it out of the maps. - /// The `type` of a frame this class wrote, for a log line; never the - /// payload, which carries positions and order details. - static String frameType(String text) { - int at = text.indexOf("\"type\":\""); - if (at < 0) { - return "a frame"; - } - at += 8; - int end = text.indexOf('"', at); - return end < 0 ? "a frame" : "a " + text.substring(at, end) + " frame"; - } - private static void send(List to, String text) { for (int iter = 0; iter < to.size(); iter++) { WebSocketSession session = to.get(iter); @@ -226,4 +214,16 @@ private static void send(List to, String text) { } } } + + /// The `type` of a frame this class wrote, for a log line; never the + /// payload, which carries positions and order details. + static String frameType(String text) { + int at = text.indexOf("\"type\":\""); + if (at < 0) { + return "a frame"; + } + at += 8; + int end = text.indexOf('"', at); + return end < 0 ? "a frame" : "a " + text.substring(at, end) + " frame"; + } } From 4748f51f7d8ac998b91a91aa6f803d250a6a11b9 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:56:11 +0200 Subject: [PATCH 3/5] Declare cn1EmbeddedExiting where the GPU header is not included Co-Authored-By: Claude Opus 5.5 --- Ports/LinuxPort/nativeSources/cn1_linux_embedded.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c b/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c index 0e62224d4e5..aeaa27dc2b4 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c @@ -142,6 +142,8 @@ static volatile int cn1Running = 1; * waiting on the EDT, which may be the thread that called exit. */ static atomic_int cn1Exiting = 0; +int cn1EmbeddedExiting(void); /* cn1_linux_gpu.h, not included without the GPU */ + int cn1EmbeddedExiting(void) { return atomic_load(&cn1Exiting); } From a50038c978740a78e163a906daae512847de4178 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Nov 2026 20:57:05 +0200 Subject: [PATCH 4/5] GPU check: stub cn1EmbeddedExiting, the display unit is not linked there Co-Authored-By: Claude Opus 5.5 --- scripts/embedded/gpu-check/gpu_check.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/embedded/gpu-check/gpu_check.c b/scripts/embedded/gpu-check/gpu_check.c index 63241453aa8..3c3c59674d9 100644 --- a/scripts/embedded/gpu-check/gpu_check.c +++ b/scripts/embedded/gpu-check/gpu_check.c @@ -53,6 +53,12 @@ typedef void (*Scene)(cairo_t* cr, cairo_surface_t* image, cairo_surface_t* target); +/* The display unit (cn1_linux_embedded.c) is not linked here; this harness + * never exits mid-frame. */ +int cn1EmbeddedExiting(void) { + return 0; +} + static void fills(cairo_t* cr, cairo_surface_t* image, cairo_surface_t* target) { (void) target; (void) image; From f36a5e47a45004541c13192fd747cdc130e8f975 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Nov 2026 22:31:53 +0200 Subject: [PATCH 5/5] Initializr: resync the Savor template with the sample Co-Authored-By: Claude Opus 5.5 --- .../src/main/resources/savor-modules.zip | Bin 3106021 -> 3108381 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/scripts/initializr/common/src/main/resources/savor-modules.zip b/scripts/initializr/common/src/main/resources/savor-modules.zip index 9b9e8080308fb7e19b68effb06bfa7b17ba274a1..9f50eedc092f064cd29deacaf62b0cea619f6df3 100644 GIT binary patch delta 5284 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 Does9D