From 7a7a3493d62bf3273ad4e6252298b90ee1846b59 Mon Sep 17 00:00:00 2001 From: Peterino2 Date: Fri, 18 Sep 2026 13:19:55 -0700 Subject: [PATCH] Preserve READY ordering after successful native initialization Reserve the lifecycle event before startup and publish it first only after startup succeeds. This keeps initial macOS and Windows preferences behind READY while failed Wayland initialization emits no READY. Verified 18 core tests, Plasma native lifecycle/pixel tests, and Windows compilation. --- src/api/app.zig | 8 ++++++-- src/core_tests.zig | 18 ++++++++++++++++++ src/runtime.zig | 21 +++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/api/app.zig b/src/api/app.zig index 88a8348..00ceed3 100644 --- a/src/api/app.zig +++ b/src/api/app.zig @@ -59,9 +59,13 @@ pub export fn uhdk_app_run( const application = runtime.fromAppHandle(handle); if (application.has_run) return abi.UNAVAILABLE; profile.thread("UHDK platform"); + const ready = runtime.prepareReady(application) catch return abi.OOM; const status = c.z_native_start(handle, abi.textSpan(application.name)); - if (status != abi.OK) return status; - runtime.simpleEvent(application, c.UHDK_EVENT_READY); + if (status != abi.OK) { + runtime.destroyEvent(ready); + return status; + } + runtime.publishReady(application, ready); application.entry = entry; application.user = user; application.running = true; diff --git a/src/core_tests.zig b/src/core_tests.zig index 0c924f5..040f40a 100644 --- a/src/core_tests.zig +++ b/src/core_tests.zig @@ -164,3 +164,21 @@ test "poll allocation failure keeps queued terminal completion retryable" { try std.testing.expectEqual(@as(abi.Status, abi.OK), app_api.uhdk_app_poll_events(handle, 0, &batch)); try std.testing.expectEqual(@as(u64, 0), batch.len); } + +test "READY precedes native startup events without being published on failed startup" { + const application = try Application.create(std.testing.allocator, std.testing.io, "startup"); + defer destroyApplication(application); + const failed = try runtime.prepareReady(application); + try std.testing.expect(application.pending_head == null); + runtime.destroyEvent(failed); + const ready = try runtime.prepareReady(application); + runtime.simpleEvent(application, c.UHDK_EVENT_PREFERENCES_CHANGED); + const preferences = application.pending_head.?; + runtime.publishReady(application, ready); + runtime.simpleEvent(application, c.UHDK_EVENT_WAKE); + try std.testing.expectEqual(@as(u32, c.UHDK_EVENT_READY), application.pending_head.?.event.type); + try std.testing.expect(ready.next == preferences); + try std.testing.expect(ready.event.time_ns <= preferences.event.time_ns); + try std.testing.expectEqual(@as(u32, c.UHDK_EVENT_WAKE), preferences.next.?.event.type); + try std.testing.expect(application.pending_tail == preferences.next); +} diff --git a/src/runtime.zig b/src/runtime.zig index 5b94b5e..ad442a5 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -152,6 +152,27 @@ pub fn copyPayload(node: *Event) !void { } } +// Reserve READY before native initialization, but expose it only after startup +// succeeds. Initial preferences/open-file events may arrive during that call. +pub fn prepareReady(application: *Application) !*Event { + const node = try application.gpa.create(Event); + node.* = .{ .gpa = application.gpa, .arena = .init(application.gpa) }; + node.event.type = c.UHDK_EVENT_READY; + node.event.struct_size = @sizeOf(c.z_event); + node.event.time_ns = now(application.io) - application.epoch; + return node; +} + +pub fn publishReady(application: *Application, node: *Event) void { + std.debug.assert(node.event.type == c.UHDK_EVENT_READY); + application.lock(); + defer application.unlock(); + node.next = application.pending_head; + application.pending_head = node; + if (application.pending_tail == null) application.pending_tail = node; + application.notify(); +} + pub fn simpleEvent(application: *Application, typ: u32) void { var event = std.mem.zeroes(c.z_event); event.type = typ;