From f07b13bb2deab5a0caae296dd8920d528398b969 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Sat, 25 Jul 2026 11:07:54 -0700 Subject: [PATCH 01/10] media: add dynamic album artwork --- docs/media-run-status.md | 19 ++- examples/now-playing/widget.tsx | 27 ++-- host/src/art_cache.zig | 244 ++++++++++++++++++++++++++++++++ host/src/media.zig | 57 +++++++- host/src/windows_host.zig | 15 ++ host/src/windows_providers.cpp | 157 ++++++++++++++++++-- host/src/windows_providers.h | 12 +- runtime/src/image_paths.zig | 149 +++++++++++++++++++ runtime/src/main.zig | 182 ++++++++++++++++++++++-- runtime/src/tree.zig | 16 ++- sdk/CONTRACT.md | 56 +++++++- sdk/index.d.ts | 1 + sdk/src/reconciler.ts | 4 +- sdk/test/reconciler.test.mjs | 6 +- 14 files changed, 898 insertions(+), 47 deletions(-) create mode 100644 host/src/art_cache.zig create mode 100644 runtime/src/image_paths.zig diff --git a/docs/media-run-status.md b/docs/media-run-status.md index e6a047cf..22e47b30 100644 --- a/docs/media-run-status.md +++ b/docs/media-run-status.md @@ -6,8 +6,8 @@ Last updated: 2026-07-25 | Layer | Branch | Parent | State | |---|---|---|---| -| 01/05 | `media/01-status-sourceapp` | `master` (`b1199b5`) | DRAFT PR #32 (`ce7e60b`) | -| 02/05 | `media/02-album-art` | layer 01 | NOT STARTED | +| 01/05 | `media/01-status-sourceapp` | `master` (`b1199b5`) | DRAFT PR #32 (`2feb700`) | +| 02/05 | `media/02-album-art` | layer 01 | IN PROGRESS | | 03/05 | `media/03-transport` | layer 02 | NOT STARTED | | 04/05 | `media/04-macos-adapter` | layer 03 | SPIKE-GATED / NOT STARTED | | 05/05 | `media/05-noro-gate` | layer 04 if proven, otherwise layer 03 | NOT STARTED | @@ -33,6 +33,9 @@ This run will not change the submodule pointer. provider poll; live title, artist, source ID, and timeline rendered. - Layer 01 visual gate: PASS. Viewed captures and per-element checklist are in `docs/media-evidence/pr01-visual.md`. +- Layer 02 recovered-work verification: host `zig build test` and runtime + `zig build test -Dweb-layer=exclude -Dtrace=off`: PASS after the unattended + session was resumed. ## Blockers and unverified gates @@ -43,11 +46,13 @@ This run will not change the submodule pointer. ## Current work -Layer 01 is committed, pushed, and open as draft PR -`https://github.com/SunkenInTime/weaver/pull/32`. +Layer 02 implementation is in progress. The host now consumes coalesced SMTC +events, publishes bounded artwork through the host-owned hash cache, and emits +optional `artPath`; the runtime has canonical art-root containment and dynamic +same-ID image re-registration. SDK/example and verification work is active. ## Next executable task -Create `media/02-album-art` from layer 01 and trace SMTC thumbnail ownership, -host cache lifecycle, runtime path containment, and dynamic image -re-registration before editing. +Run the complete layer-02 unit/type/build gates, then launch +`examples/now-playing` with Spotify for track-change art latency and the +binding visual capture/checklist. diff --git a/examples/now-playing/widget.tsx b/examples/now-playing/widget.tsx index 470bc418..ee099502 100644 --- a/examples/now-playing/widget.tsx +++ b/examples/now-playing/widget.tsx @@ -4,7 +4,7 @@ const segmentCount = 24; export default widget({ name: "Now Playing", - size: [360, 142], + size: [440, 160], anchor: { corner: "bottom-left", offset: [24, 24] }, layer: "desktop", subscribe: ["media"], @@ -13,8 +13,8 @@ export default widget({ const progress = media.durationMs > 0 ? Math.max(0, Math.min(1, media.positionMs / media.durationMs)) : 0; const filled = Math.round(progress * segmentCount); return ( - - + + {media.status.toUpperCase()} - - {media.title || "Nothing playing"} - {media.artist || media.album || (media.title ? "Unknown artist" : "Open a media app to begin")} - {media.sourceApp || "Unknown source"} - - + + {media.artPath + ? + : } + + {media.title || "Nothing playing"} + {media.artist || media.album || (media.title ? "Unknown artist" : "Open a media app to begin")} + {media.sourceApp || "Unknown source"} + + + {Array.from({ length: segmentCount }, (_, index) => ( index < filled - ? - : + ? + : ))} diff --git a/host/src/art_cache.zig b/host/src/art_cache.zig new file mode 100644 index 00000000..54791fab --- /dev/null +++ b/host/src/art_cache.zig @@ -0,0 +1,244 @@ +const std = @import("std"); + +const windows = std.os.windows; + +extern "kernel32" fn MoveFileExW( + existing_file_name: [*:0]const u16, + new_file_name: [*:0]const u16, + flags: u32, +) callconv(.winapi) windows.BOOL; + +const movefile_write_through: u32 = 0x00000008; + +pub const max_input_bytes: usize = 1024 * 1024; +pub const max_files: usize = 32; +pub const hash_hex_bytes: usize = 64; +pub const max_path_bytes: usize = 259; + +pub const Publication = struct { + path: [max_path_bytes]u8 = @splat(0), + path_len: usize = 0, + hash: [hash_hex_bytes]u8 = @splat(0), + + pub fn pathSlice(self: *const Publication) []const u8 { + return self.path[0..self.path_len]; + } +}; + +/// weaverd is the sole writer. The filesystem mtime is the LRU clock and is +/// touched only after a complete file becomes the successfully published +/// hash. +pub const Cache = struct { + io: std.Io, + allocator: std.mem.Allocator, + root: []u8, + published_hash: [hash_hex_bytes]u8 = @splat(0), + published: bool = false, + temp_nonce: u64 = 0, + + pub fn init(io: std.Io, allocator: std.mem.Allocator, root: []const u8) !Cache { + var self: Cache = .{ + .io = io, + .allocator = allocator, + .root = try allocator.dupe(u8, root), + }; + errdefer allocator.free(self.root); + try std.Io.Dir.cwd().createDirPath(io, self.root); + try self.cleanupTemps(); + try self.prune(); + return self; + } + + pub fn deinit(self: *Cache) void { + self.allocator.free(self.root); + self.* = undefined; + } + + pub fn clearPublished(self: *Cache) void { + self.published = false; + } + + pub fn publish(self: *Cache, bytes: []const u8) !?Publication { + if (bytes.len > max_input_bytes) return null; + var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(bytes, &digest, .{}); + var hash: [hash_hex_bytes]u8 = undefined; + _ = try std.fmt.bufPrint(&hash, "{x}", .{digest}); + var filename_buffer: [hash_hex_bytes + ".img".len]u8 = undefined; + const filename = try std.fmt.bufPrint(&filename_buffer, "{s}.img", .{hash}); + const final_path = try std.fs.path.join(self.allocator, &.{ self.root, filename }); + defer self.allocator.free(final_path); + if (final_path.len > max_path_bytes) return error.ArtPathTooLong; + + var cwd = std.Io.Dir.cwd(); + const exists = block: { + _ = cwd.statFile(self.io, final_path, .{}) catch |err| switch (err) { + error.FileNotFound => break :block false, + else => return err, + }; + break :block true; + }; + if (!exists) { + self.temp_nonce +%= 1; + const temp_path = try std.fmt.allocPrint( + self.allocator, + "{s}.{d}.{d}.tmp", + .{ final_path, windows.GetCurrentProcessId(), self.temp_nonce }, + ); + defer self.allocator.free(temp_path); + errdefer cwd.deleteFile(self.io, temp_path) catch {}; + { + var file = try cwd.createFile(self.io, temp_path, .{ .exclusive = true }); + defer file.close(self.io); + var write_buffer: [64 * 1024]u8 = undefined; + var writer = file.writer(self.io, &write_buffer); + try writer.interface.writeAll(bytes); + try writer.interface.flush(); + try file.sync(self.io); + } + const moved = try moveNoReplace(self.allocator, temp_path, final_path); + if (!moved) cwd.deleteFile(self.io, temp_path) catch {}; + } + + { + var file = try cwd.openFile(self.io, final_path, .{ .mode = .write_only }); + defer file.close(self.io); + try file.setTimestampsNow(self.io); + } + self.published_hash = hash; + self.published = true; + try self.prune(); + + var result: Publication = .{ .hash = hash }; + @memcpy(result.path[0..final_path.len], final_path); + result.path_len = final_path.len; + return result; + } + + fn cleanupTemps(self: *Cache) !void { + var directory = try std.Io.Dir.cwd().openDir(self.io, self.root, .{ .iterate = true }); + defer directory.close(self.io); + var iterator = directory.iterate(); + while (try iterator.next(self.io)) |entry| { + if (entry.kind != .file or !std.mem.endsWith(u8, entry.name, ".tmp")) continue; + directory.deleteFile(self.io, entry.name) catch {}; + } + } + + fn prune(self: *Cache) !void { + var count = try self.imageCount(); + while (count > max_files) : (count -= 1) { + var directory = try std.Io.Dir.cwd().openDir(self.io, self.root, .{ .iterate = true }); + defer directory.close(self.io); + var iterator = directory.iterate(); + var oldest_name: [hash_hex_bytes + ".img".len]u8 = undefined; + var oldest_len: usize = 0; + var oldest_mtime: i128 = std.math.maxInt(i128); + while (try iterator.next(self.io)) |entry| { + if (entry.kind != .file or !isImageFilename(entry.name)) continue; + const hash = entry.name[0..hash_hex_bytes]; + if (self.published and std.mem.eql(u8, hash, &self.published_hash)) continue; + const stat = directory.statFile(self.io, entry.name, .{}) catch continue; + if (stat.mtime.nanoseconds >= oldest_mtime) continue; + oldest_mtime = stat.mtime.nanoseconds; + oldest_len = entry.name.len; + @memcpy(oldest_name[0..oldest_len], entry.name); + } + if (oldest_len == 0) return error.ArtCachePinExceedsLimit; + try directory.deleteFile(self.io, oldest_name[0..oldest_len]); + } + } + + fn imageCount(self: *Cache) !usize { + var directory = try std.Io.Dir.cwd().openDir(self.io, self.root, .{ .iterate = true }); + defer directory.close(self.io); + var iterator = directory.iterate(); + var count: usize = 0; + while (try iterator.next(self.io)) |entry| { + if (entry.kind == .file and isImageFilename(entry.name)) count += 1; + } + return count; + } +}; + +fn isImageFilename(name: []const u8) bool { + if (name.len != hash_hex_bytes + ".img".len or !std.mem.endsWith(u8, name, ".img")) return false; + for (name[0..hash_hex_bytes]) |byte| { + if (!std.ascii.isHex(byte) or std.ascii.isUpper(byte)) return false; + } + return true; +} + +fn moveNoReplace(allocator: std.mem.Allocator, source: []const u8, destination: []const u8) !bool { + const source_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, source); + defer allocator.free(source_w); + const destination_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, destination); + defer allocator.free(destination_w); + if (MoveFileExW(source_w.ptr, destination_w.ptr, movefile_write_through).toBool()) return true; + return switch (windows.GetLastError()) { + .ALREADY_EXISTS, .FILE_EXISTS => false, + else => error.ArtCacheRenameFailed, + }; +} + +fn testRoot(allocator: std.mem.Allocator, suffix: []const u8) ![]u8 { + return std.fmt.allocPrint(allocator, ".zig-cache/weaver-art-cache-{d}-{s}", .{ windows.GetCurrentProcessId(), suffix }); +} + +fn testImageCount(io: std.Io, root: []const u8) !usize { + var cache: Cache = .{ .io = io, .allocator = std.testing.allocator, .root = @constCast(root) }; + return cache.imageCount(); +} + +test "art cache hash-dedupes and never exposes a partial file" { + const root = try testRoot(std.testing.allocator, "dedupe"); + defer std.testing.allocator.free(root); + std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + defer std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + var cache = try Cache.init(std.testing.io, std.testing.allocator, root); + defer cache.deinit(); + + const first = (try cache.publish("complete-image")).?; + const second = (try cache.publish("complete-image")).?; + try std.testing.expectEqualStrings(first.pathSlice(), second.pathSlice()); + try std.testing.expectEqual(@as(usize, 1), try testImageCount(std.testing.io, root)); + const contents = try std.Io.Dir.cwd().readFileAlloc(std.testing.io, first.pathSlice(), std.testing.allocator, .limited(max_input_bytes)); + defer std.testing.allocator.free(contents); + try std.testing.expectEqualStrings("complete-image", contents); +} + +test "art cache skips oversized input and cleans startup temps" { + const root = try testRoot(std.testing.allocator, "limits"); + defer std.testing.allocator.free(root); + std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + defer std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + try std.Io.Dir.cwd().createDirPath(std.testing.io, root); + const stale = try std.fs.path.join(std.testing.allocator, &.{ root, "orphan.1.tmp" }); + defer std.testing.allocator.free(stale); + try std.Io.Dir.cwd().writeFile(std.testing.io, .{ .sub_path = stale, .data = "partial" }); + var cache = try Cache.init(std.testing.io, std.testing.allocator, root); + defer cache.deinit(); + try std.testing.expectError(error.FileNotFound, std.Io.Dir.cwd().statFile(std.testing.io, stale, .{})); + const oversized = try std.testing.allocator.alloc(u8, max_input_bytes + 1); + defer std.testing.allocator.free(oversized); + @memset(oversized, 0xaa); + try std.testing.expect((try cache.publish(oversized)) == null); + try std.testing.expectEqual(@as(usize, 0), try testImageCount(std.testing.io, root)); +} + +test "art cache prunes to 32 files without pruning the published hash" { + const root = try testRoot(std.testing.allocator, "prune"); + defer std.testing.allocator.free(root); + std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + defer std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + var cache = try Cache.init(std.testing.io, std.testing.allocator, root); + defer cache.deinit(); + var published: Publication = undefined; + var payload: [16]u8 = @splat(0); + for (0..max_files + 5) |index| { + std.mem.writeInt(u64, payload[0..8], index, .little); + published = (try cache.publish(&payload)).?; + } + try std.testing.expectEqual(@as(usize, max_files), try testImageCount(std.testing.io, root)); + _ = try std.Io.Dir.cwd().statFile(std.testing.io, published.pathSlice(), .{}); +} diff --git a/host/src/media.zig b/host/src/media.zig index 9de87e71..397a1eec 100644 --- a/host/src/media.zig +++ b/host/src/media.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const art_cache = @import("art_cache.zig"); const native = @cImport({ @cInclude("windows_providers.h"); @@ -42,6 +43,8 @@ pub const Frame = struct { status: Status = .stopped, source_app: [max_source_app_bytes]u8 = @splat(0), source_app_len: usize = 0, + art_path: [max_art_path_bytes]u8 = @splat(0), + art_path_len: usize = 0, position_ms: u64 = 0, duration_ms: u64 = 0, @@ -57,12 +60,19 @@ pub const Frame = struct { pub fn sourceAppSlice(self: *const Frame) []const u8 { return self.source_app[0..self.source_app_len]; } + pub fn artPathSlice(self: *const Frame) []const u8 { + return self.art_path[0..self.art_path_len]; + } }; pub const Provider = struct { session: ?*native.WeaverMediaSession = null, + cache: ?*art_cache.Cache = null, next_open_ms: u64 = 0, next_poll_ms: u64 = 0, + current_art_path: [max_art_path_bytes]u8 = @splat(0), + current_art_path_len: usize = 0, + refresh_failure_logged: bool = false, pub fn deinit(self: *Provider) void { self.close(); @@ -87,12 +97,40 @@ pub const Provider = struct { if (now_ms < self.next_poll_ms) return null; self.next_poll_ms = now_ms + 1000; var source: native.WeaverMediaState = undefined; - const result = native.weaver_media_poll(self.session, &source); + var artwork: native.WeaverMediaArtwork = undefined; + const result = native.weaver_media_poll(self.session, &source, &artwork); + defer native.weaver_media_artwork_release(&artwork); if (result < 0) { self.close(); self.next_open_ms = now_ms + 1000; return null; } + if (artwork.refresh_failed != 0) { + if (!self.refresh_failure_logged) { + std.log.warn("SMTC media properties or thumbnail refresh failed; waiting for the next change event", .{}); + self.refresh_failure_logged = true; + } + } else { + self.refresh_failure_logged = false; + } + if (artwork.changed != 0) { + self.current_art_path_len = 0; + if (self.cache) |cache| { + cache.clearPublished(); + if (artwork.too_large != 0) { + std.log.warn("SMTC thumbnail exceeds the 1 MiB art-cache limit; omitting art", .{}); + } else if (artwork.bytes != null and artwork.length > 0) { + const publication: ?art_cache.Publication = cache.publish(artwork.bytes[0..artwork.length]) catch |err| failure: { + std.log.err("media artwork cache publication failed: {s}", .{@errorName(err)}); + break :failure null; + }; + if (publication) |published| { + @memcpy(self.current_art_path[0..published.path_len], published.path[0..published.path_len]); + self.current_art_path_len = published.path_len; + } + } + } + } if (result == 0) return .{}; var frame: Frame = .{ .status = statusFromNative(source.status), @@ -103,6 +141,7 @@ pub const Provider = struct { copyText(&frame.artist, &frame.artist_len, std.mem.sliceTo(&source.artist, 0)); copyText(&frame.album, &frame.album_len, std.mem.sliceTo(&source.album, 0)); copyText(&frame.source_app, &frame.source_app_len, std.mem.sliceTo(&source.source_app, 0)); + copyText(&frame.art_path, &frame.art_path_len, self.current_art_path[0..self.current_art_path_len]); return frame; } @@ -141,6 +180,10 @@ pub fn formatFrame(frame: *const Frame, output: []u8) ![]const u8 { try json.write(frame.status.wire()); try json.objectField("sourceApp"); try json.write(frame.sourceAppSlice()); + if (frame.art_path_len > 0) { + try json.objectField("artPath"); + try json.write(frame.artPathSlice()); + } try json.objectField("positionMs"); try json.write(frame.position_ms); try json.objectField("durationMs"); @@ -183,15 +226,15 @@ test "media frame bound covers maximum escaped fields and future art path" { frame.album_len = frame.album.len; @memset(&frame.source_app, 0x01); frame.source_app_len = frame.source_app.len; + @memset(&frame.art_path, 0x01); + frame.art_path_len = frame.art_path.len; var output: [max_media_frame_bytes]u8 = undefined; const encoded = try formatFrame(&frame, &output); - const fixed_without_art = - "{\"provider\":\"media\",\"value\":{\"title\":\"\",\"artist\":\"\",\"album\":\"\",\"playing\":false,\"status\":\"stopped\",\"sourceApp\":\"\",\"positionMs\":18446744073709551615,\"durationMs\":18446744073709551615}}\n"; try std.testing.expectEqual( - fixed_without_art.len + max_json_escape_bytes * (3 * max_text_bytes + max_source_app_bytes), + max_media_frame_fixed.len + max_json_escape_bytes * (3 * max_text_bytes + max_source_app_bytes + max_art_path_bytes), encoded.len, ); - try std.testing.expect(encoded.len <= max_media_frame_bytes); + try std.testing.expectEqual(max_media_frame_bytes, encoded.len); try std.testing.expect(std.mem.indexOf(u8, encoded, "\\u0001") != null); } @@ -227,3 +270,7 @@ test "native playback status maps to the frozen tri-state" { try std.testing.expectEqual(Status.stopped, statusFromNative(native.WEAVER_MEDIA_STATUS_STOPPED)); try std.testing.expectEqual(Status.stopped, statusFromNative(99)); } + +test "native media dirty flags start dirty and coalesce duplicate events" { + try std.testing.expectEqual(@as(c_int, 1), native.weaver_media_test_dirty_coalescing()); +} diff --git a/host/src/windows_host.zig b/host/src/windows_host.zig index c4632909..65ff9b88 100644 --- a/host/src/windows_host.zig +++ b/host/src/windows_host.zig @@ -1,5 +1,6 @@ const std = @import("std"); const supervisor = @import("supervisor.zig"); +const art_cache = @import("art_cache.zig"); const audio = @import("audio.zig"); const backoff = @import("backoff.zig"); const media = @import("media.zig"); @@ -24,6 +25,7 @@ const env_pipe_name = std.unicode.utf8ToUtf16LeStringLiteral("WEAVER_HOST_PIPE") const env_backend_file = std.unicode.utf8ToUtf16LeStringLiteral("WEAVER_BACKEND_FILE"); const env_renderer_pipe = std.unicode.utf8ToUtf16LeStringLiteral("WEAVER_RENDERER_PIPE"); const env_renderer_log = std.unicode.utf8ToUtf16LeStringLiteral("WEAVER_RENDERER_LOG"); +const env_art_cache = std.unicode.utf8ToUtf16LeStringLiteral("WEAVER_ART_CACHE"); const max_widgets = supervisor.max_widgets; const max_path_bytes: usize = 2048; const invalid_handle = c.INVALID_HANDLE_VALUE; @@ -97,6 +99,7 @@ const Host = struct { renderer_pipe_name: []const u8, renderer_log_path: []const u8, cli_script: []const u8, + art_cache_root: []const u8, force_software: bool, renderer: RendererState = .{}, slots: [max_widgets]Slot = [_]Slot{.{}} ** max_widgets, @@ -200,6 +203,11 @@ const Host = struct { defer self.allocator.free(renderer_pipe_w); if (c.SetEnvironmentVariableW(env_renderer_pipe, renderer_pipe_w.ptr) == 0) return error.SetEnvironmentFailed; } + if (slot.wants_media) { + const art_cache_w = try std.unicode.utf8ToUtf16LeAllocZ(self.allocator, self.art_cache_root); + defer self.allocator.free(art_cache_w); + if (c.SetEnvironmentVariableW(env_art_cache, art_cache_w.ptr) == 0) return error.SetEnvironmentFailed; + } const backend_hash = std.hash.Wyhash.hash(0, slot.name()); const backend_path = try std.fmt.bufPrint(&slot.platform.backend_path_buffer, "{s}.backend-{x}", .{ self.status_path, backend_hash }); slot.platform.backend_path_len = backend_path.len; @@ -210,6 +218,7 @@ const Host = struct { defer { if (pipe_name.len > 0) _ = c.SetEnvironmentVariableW(env_pipe_name, null); if (slot.wants_gpu) _ = c.SetEnvironmentVariableW(env_renderer_pipe, null); + if (slot.wants_media) _ = c.SetEnvironmentVariableW(env_art_cache, null); _ = c.SetEnvironmentVariableW(env_backend_file, null); } var startup: c.STARTUPINFOW = std.mem.zeroes(c.STARTUPINFOW); @@ -605,6 +614,10 @@ fn run(init: std.process.Init) !void { defer allocator.free(status_path); const status_temp_path = try std.fmt.allocPrint(allocator, "{s}.tmp", .{status_path}); defer allocator.free(status_temp_path); + const art_cache_root = try std.fs.path.join(allocator, &.{ directory, "artcache" }); + defer allocator.free(art_cache_root); + var media_art_cache = try art_cache.Cache.init(init.io, allocator, art_cache_root); + defer media_art_cache.deinit(); const repo_root = try repositoryRoot(allocator); defer allocator.free(repo_root); const runtime_exe = try std.fs.path.join(allocator, &.{ repo_root, "runtime", "zig-out", "bin", "weaver-widget.exe" }); @@ -630,8 +643,10 @@ fn run(init: std.process.Init) !void { .renderer_pipe_name = renderer_pipe_name, .renderer_log_path = renderer_log_path, .cli_script = cli_script, + .art_cache_root = art_cache_root, .force_software = if (init.environ_map.get("WEAVER_FORCE_SOFTWARE")) |value| std.mem.eql(u8, value, "1") else false, }; + host.media_provider.cache = &media_art_cache; defer host.audio_provider.deinit(); defer host.media_provider.deinit(); try host.loadRegistry(); diff --git a/host/src/windows_providers.cpp b/host/src/windows_providers.cpp index 5c9901ea..935b7bab 100644 --- a/host/src/windows_providers.cpp +++ b/host/src/windows_providers.cpp @@ -10,12 +10,15 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include struct WeaverAudioCapture { @@ -152,11 +155,58 @@ extern "C" int weaver_audio_poll(WeaverAudioCapture *state, float *mono, size_t return 0; } +constexpr size_t max_artwork_bytes = 1024 * 1024; + +struct WeaverMediaDirtyFlags { + std::atomic session{true}; + std::atomic properties{true}; + + void mark_session() { + session.store(true, std::memory_order_release); + } + + void mark_properties() { + properties.store(true, std::memory_order_release); + } + + bool take_session() { + return session.exchange(false, std::memory_order_acq_rel); + } + + bool take_properties() { + return properties.exchange(false, std::memory_order_acq_rel); + } +}; + struct WeaverMediaSession { winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager manager{nullptr}; + winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSession current{nullptr}; + winrt::event_token manager_token{}; + winrt::event_token properties_token{}; + bool manager_subscribed = false; + bool properties_subscribed = false; + std::atomic shutting_down{false}; + WeaverMediaDirtyFlags dirty; + std::string title; + std::string artist; + std::string album; + std::string source_app; + bool refresh_failed = false; bool apartment_initialized = false; ~WeaverMediaSession() { + shutting_down.store(true, std::memory_order_release); + try { + if (properties_subscribed && current) current.MediaPropertiesChanged(properties_token); + } catch (...) { + } + try { + if (manager_subscribed && manager) manager.CurrentSessionChanged(manager_token); + } catch (...) { + } + properties_subscribed = false; + manager_subscribed = false; + current = nullptr; manager = nullptr; if (apartment_initialized) winrt::uninit_apartment(); } @@ -190,6 +240,60 @@ static std::string source_app_name(const winrt::hstring &source_id) { } } +static void clear_cached_properties(WeaverMediaSession *state) { + state->title.clear(); + state->artist.clear(); + state->album.clear(); + state->source_app.clear(); +} + +static void rebind_current_session(WeaverMediaSession *state) { + if (state->properties_subscribed && state->current) { + state->current.MediaPropertiesChanged(state->properties_token); + } + state->properties_subscribed = false; + state->current = nullptr; + clear_cached_properties(state); + state->current = state->manager.GetCurrentSession(); + if (state->current) { + state->source_app = source_app_name(state->current.SourceAppUserModelId()); + state->properties_token = state->current.MediaPropertiesChanged( + [state](const auto &, const auto &) { + if (!state->shutting_down.load(std::memory_order_acquire)) state->dirty.mark_properties(); + }); + state->properties_subscribed = true; + } + state->dirty.mark_properties(); +} + +static bool read_thumbnail( + const winrt::Windows::Storage::Streams::IRandomAccessStreamReference &reference, + WeaverMediaArtwork *artwork) { + artwork->changed = 1; + if (!reference) return true; + try { + const auto stream = reference.OpenReadAsync().get(); + const uint64_t size = stream.Size(); + if (size > max_artwork_bytes) { + artwork->too_large = 1; + return true; + } + if (size == 0) return true; + const auto input = stream.GetInputStreamAt(0); + winrt::Windows::Storage::Streams::DataReader reader(input); + const uint32_t loaded = reader.LoadAsync(static_cast(size)).get(); + if (loaded != size) return false; + auto bytes = std::unique_ptr(new (std::nothrow) uint8_t[loaded]); + if (!bytes) return false; + reader.ReadBytes(winrt::array_view(bytes.get(), bytes.get() + loaded)); + artwork->bytes = bytes.release(); + artwork->length = loaded; + return true; + } catch (...) { + return false; + } +} + extern "C" void weaver_media_select_source_app(const char *raw_id, const char *resolved_name, char output[257]) { if (!output) return; const std::string selected = select_source_app(raw_id ? raw_id : "", resolved_name ? resolved_name : ""); @@ -204,6 +308,11 @@ extern "C" WeaverMediaSession *weaver_media_create(void) { winrt::init_apartment(winrt::apartment_type::multi_threaded); state->apartment_initialized = true; state->manager = winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get(); + state->manager_token = state->manager.CurrentSessionChanged( + [raw = state.get()](const auto &, const auto &) { + if (!raw->shutting_down.load(std::memory_order_acquire)) raw->dirty.mark_session(); + }); + state->manager_subscribed = true; return state.release(); } catch (...) { return nullptr; @@ -212,17 +321,34 @@ extern "C" WeaverMediaSession *weaver_media_create(void) { extern "C" void weaver_media_destroy(WeaverMediaSession *session) { delete session; } -extern "C" int weaver_media_poll(WeaverMediaSession *state, WeaverMediaState *output) { - if (!state || !output) return -1; +extern "C" int weaver_media_poll(WeaverMediaSession *state, WeaverMediaState *output, WeaverMediaArtwork *artwork) { + if (!state || !output || !artwork) return -1; std::memset(output, 0, sizeof(*output)); + std::memset(artwork, 0, sizeof(*artwork)); try { - const auto session = state->manager.GetCurrentSession(); + const bool session_changed = state->dirty.take_session(); + if (session_changed) { + rebind_current_session(state); + artwork->changed = 1; + } + const auto session = state->current; if (!session) return 0; - const auto properties = session.TryGetMediaPropertiesAsync().get(); - copy_text(output->title, properties.Title()); - copy_text(output->artist, properties.Artist()); - copy_text(output->album, properties.AlbumTitle()); - copy_text(output->source_app, source_app_name(session.SourceAppUserModelId())); + if (state->dirty.take_properties()) { + try { + const auto properties = session.TryGetMediaPropertiesAsync().get(); + state->title = winrt::to_string(properties.Title()); + state->artist = winrt::to_string(properties.Artist()); + state->album = winrt::to_string(properties.AlbumTitle()); + state->refresh_failed = !read_thumbnail(properties.Thumbnail(), artwork); + } catch (...) { + state->refresh_failed = true; + } + } + artwork->refresh_failed = state->refresh_failed ? 1 : 0; + copy_text(output->title, state->title); + copy_text(output->artist, state->artist); + copy_text(output->album, state->album); + copy_text(output->source_app, state->source_app); const auto playback = session.GetPlaybackInfo(); switch (playback.PlaybackStatus()) { case winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing: @@ -246,3 +372,18 @@ extern "C" int weaver_media_poll(WeaverMediaSession *state, WeaverMediaState *ou return -2; } } + +extern "C" void weaver_media_artwork_release(WeaverMediaArtwork *artwork) { + if (!artwork) return; + delete[] artwork->bytes; + std::memset(artwork, 0, sizeof(*artwork)); +} + +extern "C" int weaver_media_test_dirty_coalescing(void) { + WeaverMediaDirtyFlags dirty; + if (!dirty.take_session() || dirty.take_session()) return 0; + if (!dirty.take_properties() || dirty.take_properties()) return 0; + dirty.mark_properties(); + dirty.mark_properties(); + return dirty.take_properties() && !dirty.take_properties(); +} diff --git a/host/src/windows_providers.h b/host/src/windows_providers.h index c716e9a9..138f0f5c 100644 --- a/host/src/windows_providers.h +++ b/host/src/windows_providers.h @@ -32,10 +32,20 @@ typedef struct WeaverMediaState { typedef struct WeaverMediaSession WeaverMediaSession; +typedef struct WeaverMediaArtwork { + uint8_t *bytes; + size_t length; + int changed; + int too_large; + int refresh_failed; +} WeaverMediaArtwork; + WeaverMediaSession *weaver_media_create(void); void weaver_media_destroy(WeaverMediaSession *session); -int weaver_media_poll(WeaverMediaSession *session, WeaverMediaState *state); +int weaver_media_poll(WeaverMediaSession *session, WeaverMediaState *state, WeaverMediaArtwork *artwork); +void weaver_media_artwork_release(WeaverMediaArtwork *artwork); void weaver_media_select_source_app(const char *raw_id, const char *resolved_name, char output[257]); +int weaver_media_test_dirty_coalescing(void); #ifdef __cplusplus } diff --git a/runtime/src/image_paths.zig b/runtime/src/image_paths.zig new file mode 100644 index 00000000..0b1fdffb --- /dev/null +++ b/runtime/src/image_paths.zig @@ -0,0 +1,149 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const media_protocol = @import("media_protocol.zig"); + +pub const Kind = enum { widget_asset, host_art }; + +pub const Resolved = struct { + path: [:0]u8, + kind: Kind, +}; + +pub const Resolver = struct { + io: std.Io, + allocator: std.mem.Allocator, + widget_root: [:0]u8, + art_root: ?[:0]u8, + + pub fn init( + io: std.Io, + allocator: std.mem.Allocator, + widget_directory: []const u8, + art_cache_environment: ?[]const u8, + ) !Resolver { + const widget_root = try std.Io.Dir.cwd().realPathFileAlloc(io, widget_directory, allocator); + errdefer allocator.free(widget_root); + var art_root: ?[:0]u8 = null; + if (art_cache_environment) |root| { + if (!validAbsoluteArtPath(root)) return error.InvalidArtCacheRoot; + art_root = try std.Io.Dir.cwd().realPathFileAlloc(io, root, allocator); + } + return .{ + .io = io, + .allocator = allocator, + .widget_root = widget_root, + .art_root = art_root, + }; + } + + pub fn deinit(self: *Resolver) void { + self.allocator.free(self.widget_root); + if (self.art_root) |root| self.allocator.free(root); + self.* = undefined; + } + + pub fn resolve(self: *const Resolver, source: []const u8) !Resolved { + if (isLocalAssetPath(source)) { + const relative = if (std.mem.startsWith(u8, source, "./") or std.mem.startsWith(u8, source, ".\\")) source[2..] else source; + const joined = try std.fs.path.join(self.allocator, &.{ self.widget_root, relative }); + defer self.allocator.free(joined); + const resolved = try std.Io.Dir.cwd().realPathFileAlloc(self.io, joined, self.allocator); + errdefer self.allocator.free(resolved); + if (!componentContained(self.widget_root, resolved, builtin.os.tag == .windows)) return error.WidgetAssetEscapesRoot; + return .{ .path = resolved, .kind = .widget_asset }; + } + const root = self.art_root orelse return error.InvalidImageSource; + if (!validAbsoluteArtPath(source) or hasParentComponent(source)) return error.InvalidImageSource; + const resolved = try std.Io.Dir.realPathFileAbsoluteAlloc(self.io, source, self.allocator); + errdefer self.allocator.free(resolved); + if (!componentContained(root, resolved, builtin.os.tag == .windows)) return error.ArtPathOutsideCache; + if (resolved.len > media_protocol.max_art_path_bytes) return error.ArtPathTooLong; + return .{ .path = resolved, .kind = .host_art }; + } +}; + +/// This is the original startup-asset rule. The startup loader continues to +/// use it unchanged; dynamic resolution adds the one host-cache acceptance. +pub fn isLocalAssetPath(source: []const u8) bool { + if (source.len == 0 or std.fs.path.isAbsolute(source) or std.mem.indexOf(u8, source, "://") != null) return false; + var components = std.mem.tokenizeAny(u8, source, "/\\"); + while (components.next()) |component| if (std.mem.eql(u8, component, "..")) return false; + return true; +} + +fn hasParentComponent(path: []const u8) bool { + var components = std.mem.tokenizeAny(u8, path, "/\\"); + while (components.next()) |component| if (std.mem.eql(u8, component, "..")) return true; + return false; +} + +fn validAbsoluteArtPath(path: []const u8) bool { + if (path.len == 0 or std.mem.indexOf(u8, path, "://") != null) return false; + if (builtin.os.tag != .windows) return std.fs.path.isAbsolute(path); + // Only ordinary drive-absolute paths are accepted. This excludes + // drive-relative, rooted-current-drive, UNC, and both device namespaces. + return path.len >= 3 and std.ascii.isAlphabetic(path[0]) and path[1] == ':' and isSeparator(path[2]); +} + +fn isSeparator(byte: u8) bool { + return byte == '/' or byte == '\\'; +} + +fn componentContained(root: []const u8, candidate: []const u8, case_insensitive: bool) bool { + var root_components = std.mem.tokenizeAny(u8, root, "/\\"); + var candidate_components = std.mem.tokenizeAny(u8, candidate, "/\\"); + while (root_components.next()) |root_component| { + const candidate_component = candidate_components.next() orelse return false; + const equal = if (case_insensitive) + std.ascii.eqlIgnoreCase(root_component, candidate_component) + else + std.mem.eql(u8, root_component, candidate_component); + if (!equal) return false; + } + // An image must be a child of the cache root, not the root itself. + return candidate_components.next() != null; +} + +fn windowsLexicallyContained(root: []const u8, candidate: []const u8) bool { + if (candidate.len < 3 or !std.ascii.isAlphabetic(candidate[0]) or candidate[1] != ':' or !isSeparator(candidate[2])) return false; + if (hasParentComponent(candidate)) return false; + return componentContained(root, candidate, true); +} + +test "Windows art containment rejects traversal prefixes and namespaces" { + const root = "C:\\Users\\Dara\\AppData\\Local\\weaver\\artcache"; + try std.testing.expect(windowsLexicallyContained(root, "c:/users/DARA/AppData/Local/WEAVER/artcache/abc.img")); + try std.testing.expect(windowsLexicallyContained(root, "C:\\Users\\Dara/AppData\\Local/weaver\\artcache\\abc.img")); + try std.testing.expect(!windowsLexicallyContained(root, "C:\\Users\\Dara\\AppData\\Local\\weaver\\artcache\\..\\x.img")); + try std.testing.expect(!windowsLexicallyContained(root, "C:\\Users\\Dara\\AppData\\Local\\weaver\\artcache-evil\\x.img")); + try std.testing.expect(!windowsLexicallyContained(root, "C:artcache\\x.img")); + try std.testing.expect(!windowsLexicallyContained(root, "\\\\server\\share\\x.img")); + try std.testing.expect(!windowsLexicallyContained(root, "\\\\?\\C:\\Users\\Dara\\AppData\\Local\\weaver\\artcache\\x.img")); + try std.testing.expect(!windowsLexicallyContained(root, "\\\\.\\C:\\Users\\Dara\\AppData\\Local\\weaver\\artcache\\x.img")); +} + +test "resolver accepts only a real file under the canonical host art root" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + const root = ".zig-cache/weaver-image-path-test"; + std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + defer std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + const widget_root = try std.fs.path.join(std.testing.allocator, &.{ root, "widget" }); + defer std.testing.allocator.free(widget_root); + const art_root = try std.fs.path.join(std.testing.allocator, &.{ root, "artcache" }); + defer std.testing.allocator.free(art_root); + try std.Io.Dir.cwd().createDirPath(std.testing.io, widget_root); + try std.Io.Dir.cwd().createDirPath(std.testing.io, art_root); + const art = try std.fs.path.join(std.testing.allocator, &.{ art_root, "abc.img" }); + defer std.testing.allocator.free(art); + try std.Io.Dir.cwd().writeFile(std.testing.io, .{ .sub_path = art, .data = "image" }); + const absolute_art_root = try std.Io.Dir.cwd().realPathFileAlloc(std.testing.io, art_root, std.testing.allocator); + defer std.testing.allocator.free(absolute_art_root); + const absolute_art = try std.Io.Dir.cwd().realPathFileAlloc(std.testing.io, art, std.testing.allocator); + defer std.testing.allocator.free(absolute_art); + var resolver = try Resolver.init(std.testing.io, std.testing.allocator, widget_root, absolute_art_root); + defer resolver.deinit(); + const resolved = try resolver.resolve(absolute_art); + defer std.testing.allocator.free(resolved.path); + try std.testing.expectEqual(Kind.host_art, resolved.kind); + try std.testing.expectEqualStrings(absolute_art, resolved.path); +} diff --git a/runtime/src/main.zig b/runtime/src/main.zig index f5396d97..b3d7e4b8 100644 --- a/runtime/src/main.zig +++ b/runtime/src/main.zig @@ -4,6 +4,7 @@ const runner = @import("runner"); const native_sdk = @import("native_sdk"); const geometry_mod = @import("geometry.zig"); const dev_reload = @import("dev_reload.zig"); +const image_paths = @import("image_paths.zig"); const js_engine = @import("js_engine.zig"); const manifest_mod = @import("manifest.zig"); const provider_mod = @import("provider.zig"); @@ -46,6 +47,11 @@ pub const Model = struct { slider_values: [tree_mod.max_nodes]f32 = @splat(0), images: [max_images]ImageAsset = [_]ImageAsset{.{}} ** max_images, image_count: usize = 0, + image_states: [max_images]ImageState = [_]ImageState{.{}} ** max_images, + image_state_count: usize = 0, + image_tree_generation: u64 = 0, + image_epoch: u64 = 1, + image_resolver: ?*const image_paths.Resolver = null, geometry: ?*const geometry_mod.Store = null, fonts: []const manifest_mod.Font = &.{}, /// Last origin we consider settled (launch placement or the last @@ -64,6 +70,14 @@ const fetch_poll_key: u64 = 0x7766_6574_6368; const provider_poll_key: u64 = 0x7770_726f_7669; const geometry_save_key: u64 = 0x7767_656f_6d65; const ImageAsset = struct { id: u64 = 0, bytes: []const u8 = &.{} }; +const ImageState = struct { + id: tree_mod.NodeId = 0, + lifetime: u64 = 0, + epoch: u64 = 0, + observed: ?[:0]u8 = null, + observed_valid: bool = false, + registered: bool = false, +}; pub const Msg = union(enum) { timer: native_sdk.EffectTimer, @@ -95,7 +109,9 @@ fn initEffects(model: *Model, effects: *Effects) void { for (model.images[0..model.image_count]) |image| { _ = effects.registerImageBytes(image.id, image.bytes) catch |err| { std.log.err("widget image {d} failed to decode/register: {s}", .{ image.id, @errorName(err) }); + continue; }; + if (findImageState(model, @intCast(image.id))) |state| state.registered = true; } syncTimers(model, effects); } @@ -103,6 +119,9 @@ fn initEffects(model: *Model, effects: *Effects) void { /// One SDK timer delivery is one JS batch. All retained-tree ops complete /// before update returns, after which UiApp derives and presents once. fn update(model: *Model, msg: Msg, effects: *Effects) void { + defer synchronizeImages(model, effects) catch |err| { + std.log.err("widget image synchronization failed: {s}", .{@errorName(err)}); + }; switch (msg) { .timer => |timer| { if (timer.outcome != .fired) { @@ -121,14 +140,14 @@ fn update(model: *Model, msg: Msg, effects: *Effects) void { return; } if (timer.key == provider_poll_key) { - drainProviderFrames(model) catch |err| { + drainProviderFrames(model, effects) catch |err| { std.log.err("widget provider dispatch failed: {s}", .{@errorName(err)}); }; syncTimers(model, effects); return; } if (model.provider_poll_interval_ms <= 33) { - drainProviderFrames(model) catch |err| { + drainProviderFrames(model, effects) catch |err| { std.log.err("widget provider dispatch failed: {s}", .{@errorName(err)}); }; } @@ -177,7 +196,7 @@ fn update(model: *Model, msg: Msg, effects: *Effects) void { }, .canvas_frame => |timestamp_ns| { if (model.provider_poll_interval_ms <= 33) { - drainProviderFrames(model) catch |err| { + drainProviderFrames(model, effects) catch |err| { std.log.err("widget provider dispatch failed: {s}", .{@errorName(err)}); }; } @@ -267,7 +286,7 @@ fn onWindowFrame(event: native_sdk.runtime.WindowFrameEvent) ?Msg { } }; } -fn drainProviderFrames(model: *Model) !void { +fn drainProviderFrames(model: *Model, _: *Effects) !void { const count = try (model.engine orelse return).drainProviders(); if (count == 0) return; model.provider_frames += count; @@ -301,6 +320,8 @@ fn reloadIfChanged(model: *Model, effects: *Effects) !void { else => return err, }; candidate_tree.generation = model.tree.generation +% 1; + model.image_epoch +%= 1; + if (model.image_epoch == 0) model.image_epoch = 1; model.tree.deinit(); model.tree = candidate_tree; candidate_tree_moved = true; @@ -770,7 +791,7 @@ fn loadLocalImages(io: std.Io, allocator: std.mem.Allocator, directory: []const for (&model.tree.nodes, 0..) |*node, index| { if (!node.alive or node.kind != .image) continue; const source = node.sourceSlice(); - if (!isLocalAssetPath(source)) { + if (!image_paths.isLocalAssetPath(source)) { std.log.err("RemoteImageUnsupported: remote sources arrive in M3; use a local widget path", .{}); return error.RemoteImageUnsupported; } @@ -783,11 +804,142 @@ fn loadLocalImages(io: std.Io, allocator: std.mem.Allocator, directory: []const } } -fn isLocalAssetPath(source: []const u8) bool { - if (source.len == 0 or std.fs.path.isAbsolute(source) or std.mem.indexOf(u8, source, "://") != null) return false; - var components = std.mem.tokenizeAny(u8, source, "/\\"); - while (components.next()) |component| if (std.mem.eql(u8, component, "..")) return false; - return true; +fn seedImageStates(model: *Model) !void { + const resolver = model.image_resolver orelse return; + for (model.images[0..model.image_count]) |image| { + const id: tree_mod.NodeId = @intCast(image.id); + const node = try model.tree.nodeConst(id); + const resolved = try resolver.resolve(node.sourceSlice()); + const state = try addImageState(model, id, node.lifetime); + state.observed = resolved.path; + state.observed_valid = true; + } + model.image_tree_generation = model.tree.generation; +} + +fn deinitImageStates(model: *Model) void { + for (model.image_states[0..model.image_state_count]) |*state| { + if (state.observed) |observed| std.heap.page_allocator.free(observed); + } + model.image_state_count = 0; +} + +fn findImageState(model: *Model, id: tree_mod.NodeId) ?*ImageState { + for (model.image_states[0..model.image_state_count]) |*state| { + if (state.id == id) return state; + } + return null; +} + +fn addImageState(model: *Model, id: tree_mod.NodeId, lifetime: u64) !*ImageState { + if (model.image_state_count == model.image_states.len) return error.TooManyImages; + const state = &model.image_states[model.image_state_count]; + state.* = .{ .id = id, .lifetime = lifetime, .epoch = model.image_epoch }; + model.image_state_count += 1; + return state; +} + +fn removeImageState(model: *Model, effects: *Effects, index: usize) void { + const state = &model.image_states[index]; + if (state.registered) _ = effects.unregisterImage(state.id); + if (state.observed) |observed| std.heap.page_allocator.free(observed); + model.image_state_count -= 1; + if (index != model.image_state_count) model.image_states[index] = model.image_states[model.image_state_count]; + model.image_states[model.image_state_count] = .{}; +} + +fn replaceObserved(state: *ImageState, observed: [:0]u8, valid: bool) void { + if (state.observed) |previous| std.heap.page_allocator.free(previous); + state.observed = observed; + state.observed_valid = valid; +} + +fn rememberRawSource(state: *ImageState, source: []const u8) !void { + replaceObserved(state, try std.heap.page_allocator.dupeZ(u8, source), false); +} + +fn observedEquals(state: *const ImageState, source: []const u8, valid: bool) bool { + return state.observed_valid == valid and if (state.observed) |observed| std.mem.eql(u8, observed, source) else false; +} + +fn invalidImageSourceError(err: anyerror) bool { + return switch (err) { + error.InvalidImageSource, + error.ArtPathOutsideCache, + error.WidgetAssetEscapesRoot, + error.ArtPathTooLong, + => true, + else => false, + }; +} + +fn synchronizeImageNode(model: *Model, effects: *Effects, id: tree_mod.NodeId, node: *const tree_mod.Node) !void { + const resolver = model.image_resolver orelse return; + var state = findImageState(model, id) orelse try addImageState(model, id, node.lifetime); + const resolved = resolver.resolve(node.sourceSlice()) catch |err| { + if (observedEquals(state, node.sourceSlice(), false)) return; + if (invalidImageSourceError(err) and state.registered) { + _ = effects.unregisterImage(id); + state.registered = false; + } + try rememberRawSource(state, node.sourceSlice()); + if (invalidImageSourceError(err)) { + std.log.err("image source rejected by widget/art-cache containment: {s}", .{@errorName(err)}); + } else { + std.log.err("image source could not be resolved; keeping the prior image: {s}", .{@errorName(err)}); + } + return; + }; + if (observedEquals(state, resolved.path, true)) { + std.heap.page_allocator.free(resolved.path); + return; + } + const bytes = std.Io.Dir.cwd().readFileAlloc( + model.io orelse return error.MissingIo, + resolved.path, + std.heap.page_allocator, + .limited(1024 * 1024), + ) catch |err| { + replaceObserved(state, resolved.path, true); + std.log.err("image reload read failed; keeping the prior image: {s}", .{@errorName(err)}); + return; + }; + defer std.heap.page_allocator.free(bytes); + _ = effects.registerImageBytes(id, bytes) catch |err| { + replaceObserved(state, resolved.path, true); + std.log.err("image reload decode/register failed; keeping the prior image: {s}", .{@errorName(err)}); + return; + }; + replaceObserved(state, resolved.path, true); + state.registered = true; +} + +/// Runs after every app-loop turn, but the generation equality is the static +/// fast path. A changed image is decoded before its same-ID resource is +/// replaced; invalid/dead/reused nodes are unregistered explicitly. +fn synchronizeImages(model: *Model, effects: *Effects) !void { + if (model.tree.generation == model.image_tree_generation) return; + const synchronized_generation = model.tree.generation; + + var state_index: usize = 0; + while (state_index < model.image_state_count) { + const state = &model.image_states[state_index]; + const node = model.tree.nodeConst(state.id) catch { + removeImageState(model, effects, state_index); + continue; + }; + if (node.kind != .image or node.lifetime != state.lifetime or state.epoch != model.image_epoch) { + removeImageState(model, effects, state_index); + continue; + } + state_index += 1; + } + + for (&model.tree.nodes, 0..) |*node, index| { + if (!node.alive or node.kind != .image) continue; + try synchronizeImageNode(model, effects, @intCast(index + 1), node); + } + model.image_tree_generation = synchronized_generation; } pub fn main(init: std.process.Init) !void { @@ -869,6 +1021,13 @@ pub fn main(init: std.process.Init) !void { }}; const scene: native_sdk.ShellConfig = .{ .windows = &shell_windows }; const fonts = try loadFonts(init.io, allocator, directory, loaded.manifest.fonts); + var image_resolver = try image_paths.Resolver.init( + init.io, + std.heap.page_allocator, + directory, + init.environ_map.get("WEAVER_ART_CACHE"), + ); + defer image_resolver.deinit(); var tokens = native_sdk.canvas.DesignTokens.theme(.{ .pack = .geist, .color_scheme = .dark }); tokens.colors.background = native_sdk.canvas.Color.rgba8(0, 0, 0, 0); @@ -888,8 +1047,10 @@ pub fn main(init: std.process.Init) !void { }); defer app_state.destroy(); defer app_state.model.tree.deinit(); + defer deinitImageStates(&app_state.model); app_state.model.geometry = &geometry_store; app_state.model.fonts = loaded.manifest.fonts; + app_state.model.image_resolver = &image_resolver; if (dragged) |saved| app_state.model.frame_origin = .{ saved.x, saved.y }; try app_state.model.provider.init(init.io, platform.providerEndpoint( init.environ_map.get("WEAVER_HOST_PIPE"), @@ -909,6 +1070,7 @@ pub fn main(init: std.process.Init) !void { } try engine.evaluate(loaded.bundle, "bundle.js"); try loadLocalImages(init.io, allocator, directory, &app_state.model); + try seedImageStates(&app_state.model); requested_software_backend = renderer_backend == .software; const dev_signal_path = try std.fs.path.join(allocator, &.{ directory, dev_reload.signal_file_name }); diff --git a/runtime/src/tree.zig b/runtime/src/tree.zig index 6ddacadb..bdeade3f 100644 --- a/runtime/src/tree.zig +++ b/runtime/src/tree.zig @@ -66,6 +66,7 @@ pub const InteractionStyle = struct { /// cannot silently turn bridge traffic into an unbounded native heap. pub const Node = struct { alive: bool = false, + lifetime: u64 = 0, kind: Kind = .column, parent: ?NodeId = null, children: [max_children]NodeId = @splat(0), @@ -200,6 +201,7 @@ pub const Tree = struct { canvases: [max_canvases]CanvasState = [_]CanvasState{.{}} ** max_canvases, root: ?NodeId = null, generation: u64 = 0, + next_node_lifetime: u64 = 1, batch_depth: u8 = 0, batch_changed: bool = false, @@ -233,7 +235,9 @@ pub const Tree = struct { if (canvas.owner == 0) break candidate; } else return error.CanvasLimit; } - slot.* = .{ .alive = true, .kind = kind }; + slot.* = .{ .alive = true, .lifetime = self.next_node_lifetime, .kind = kind }; + self.next_node_lifetime +%= 1; + if (self.next_node_lifetime == 0) self.next_node_lifetime = 1; if (canvas_index) |canvas_slot| { const id: NodeId = @intCast(index + 1); self.canvases[canvas_slot] = .{ .owner = id }; @@ -819,6 +823,16 @@ pub const Tree = struct { } }; +test "node lifetime changes when a removed image id is reused" { + var tree: Tree = .{}; + const first = try tree.createNode(.image); + const first_lifetime = (try tree.nodeConst(first)).lifetime; + try tree.removeNode(first); + const second = try tree.createNode(.image); + try std.testing.expectEqual(first, second); + try std.testing.expect((try tree.nodeConst(second)).lifetime != first_lifetime); +} + fn appendCanvasCommand(canvas: *CanvasState, command: native_sdk.canvas.ImmediateCanvasCommand) Error!void { if (canvas.command_count == max_canvas_commands) return error.CanvasCommandLimit; canvas.commands[canvas.command_count] = command; diff --git a/sdk/CONTRACT.md b/sdk/CONTRACT.md index af8b0bec..4647b51c 100644 --- a/sdk/CONTRACT.md +++ b/sdk/CONTRACT.md @@ -584,5 +584,57 @@ timeline values. Host formatting, host dedupe storage, and runtime line accumulators use this same bound. Encoding failure is logged and never silently dropped. -Album art and media transport are deliberately absent at this layer; they land -in subsequent v0.5 stack amendments. +Media transport is deliberately absent at this layer; it lands in a subsequent +v0.5 stack amendment. + +## PR 02: album art and dynamic image sources + +`MediaData` gains one optional field: + +```ts +interface MediaData { + artPath?: string; +} +``` + +The field is absent when the platform or current session supplies no artwork. +When present it is an opaque absolute path into the host-owned art cache. +Widgets must use conditional rendering because `.src` remains required: + +```tsx +{media.artPath + ? + : } +``` + +weaverd is the cache's sole writer. On Windows the root is +`%LOCALAPPDATA%\weaver\artcache`, inheriting the per-user LocalAppData access +boundary; the runtime receives that one root in `WEAVER_ART_CACHE` only for +widgets subscribed to media. Filenames are lowercase SHA-256 hex plus `.img`. +Inputs over 1 MiB are skipped. A new file is written to a unique exclusive +temporary file, flushed and synchronized, then atomically renamed without +replacement. Temporary files are removed on failure and startup. Only a +complete successfully published file can enter a provider frame. Successful +publication updates the LRU timestamp; pruning retains at most 32 images and +never removes the currently published hash. + +SMTC manager/session property events only set coalescing dirty flags. The +existing one-second host poll consumes those flags, refreshes metadata and art, +and continues to sample playback/timeline state. Event callbacks never perform +I/O or block. Refresh failure clears the consumed flag, preserves the prior +snapshot, logs once, and retries only after another property change. + +Static widget images keep their startup load and registration path. After each +JS turn that can mutate the retained tree, the runtime compares image node +lifetimes and normalized sources. A changed source is read and decoded before +replacing the same node/resource ID, so a transient read or decode failure +keeps the prior image. Removed, non-image, invalid-source, hot-swapped, and +reused-ID nodes unregister stale resources. An unchanged tree exits by one +generation comparison. + +The runtime canonicalizes the widget root and host art-cache root once. Widget +assets must remain within the widget root. Host art paths must be ordinary +drive-absolute Windows paths within the cache at a case-insensitive component +boundary. Drive-relative, rooted-current-drive, UNC, device-namespace, `..`, +missing, and prefix-collision paths are rejected. Widgets cannot nominate an +additional readable root. diff --git a/sdk/index.d.ts b/sdk/index.d.ts index 53993116..7bde8031 100644 --- a/sdk/index.d.ts +++ b/sdk/index.d.ts @@ -41,6 +41,7 @@ export interface MediaData { status: "playing" | "paused" | "stopped"; playing: boolean; sourceApp: string; + artPath?: string; positionMs: number; durationMs: number; } diff --git a/sdk/src/reconciler.ts b/sdk/src/reconciler.ts index 44d5234e..a6e81635 100644 --- a/sdk/src/reconciler.ts +++ b/sdk/src/reconciler.ts @@ -45,6 +45,7 @@ export interface MediaData { status: "playing" | "paused" | "stopped"; playing: boolean; sourceApp: string; + artPath?: string; positionMs: number; durationMs: number; } @@ -511,7 +512,8 @@ function applyElementProps(instance: HostInstance, props: Record requires a local src string"); - if (/^[a-z][a-z0-9+.-]*:/i.test(props.src) || props.src.startsWith("//")) { + const windowsAbsolute = /^[a-z]:[\\/]/i.test(props.src); + if ((!windowsAbsolute && /^[a-z][a-z0-9+.-]*:/i.test(props.src)) || props.src.startsWith("//")) { throw new Error("RemoteImageUnsupported: remote sources arrive in M3; use a local widget path"); } next.src = props.src; diff --git a/sdk/test/reconciler.test.mjs b/sdk/test/reconciler.test.mjs index fb723e6b..316a40cc 100644 --- a/sdk/test/reconciler.test.mjs +++ b/sdk/test/reconciler.test.mjs @@ -79,6 +79,9 @@ test("widget renders one native generation and providers use native timers", asy sdk.h("text", null, audio.bands[0].toFixed(2)), sdk.h("text", null, media.title), sdk.h("text", null, `${media.status}:${media.sourceApp}`), + media.artPath + ? sdk.h("image", { src: media.artPath, fit: "cover", class: "w-6 h-6" }) + : sdk.h("panel", { class: "w-6 h-6 bg-zinc-800" }), sdk.h("icon", { iconPath: "M 5 5 L 19 12 L 5 19 Z", iconViewBox: "0 0 24 24", @@ -205,12 +208,13 @@ test("widget renders one native generation and providers use native timers", asy assert.equal(typeof providerCallback, "function"); providerCallback('{"provider":"cpu","value":{"percent":37.5,"perCore":[30,45]}}'); providerCallback('{"provider":"audio","value":{"rms":0.25,"bands":[0.75]}}'); - providerCallback('{"provider":"media","value":{"title":"Test Song","artist":"Artist","album":"Album","playing":true,"status":"playing","sourceApp":"Test Player","positionMs":10,"durationMs":20}}'); + providerCallback('{"provider":"media","value":{"title":"Test Song","artist":"Artist","album":"Album","playing":true,"status":"playing","sourceApp":"Test Player","artPath":"C:\\\\Users\\\\test\\\\AppData\\\\Local\\\\weaver\\\\artcache\\\\abc.img","positionMs":10,"durationMs":20}}'); await Promise.resolve(); assert.ok(operations.some((operation) => operation[0] === "setText" && operation[2] === "37.5")); assert.ok(operations.some((operation) => operation[0] === "setText" && operation[2] === "0.75")); assert.ok(operations.some((operation) => operation[0] === "setText" && operation[2] === "Test Song")); assert.ok(operations.some((operation) => operation[0] === "setText" && operation[2] === "playing:Test Player")); + assert.ok(operations.some((operation) => operation[0] === "setProp" && operation[2] === "source" && operation[3] === "C:\\Users\\test\\AppData\\Local\\weaver\\artcache\\abc.img")); const canvasNodes = operations.filter((operation) => operation[0] === "createNode" && operation[1] === "canvas"); const pausedCanvasNode = canvasNodes[0][2]; const canvasNode = canvasNodes[1][2]; From ceea8cd25edef0566136f914d758f9cb3588e186 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Sat, 25 Jul 2026 11:15:35 -0700 Subject: [PATCH 02/10] media: normalize artwork to the widget image budget --- .../pr02-now-playing-paused.png | Bin 0 -> 49256 bytes .../pr02-now-playing-track-change.png | Bin 0 -> 49237 bytes docs/media-evidence/pr02-now-playing.png | Bin 0 -> 51242 bytes docs/media-evidence/pr02-visual.md | 74 +++++++++++++ docs/media-run-status.md | 16 ++- host/build.zig | 2 + host/src/windows_providers.cpp | 99 +++++++++++++++++- sdk/CONTRACT.md | 7 ++ 8 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 docs/media-evidence/pr02-now-playing-paused.png create mode 100644 docs/media-evidence/pr02-now-playing-track-change.png create mode 100644 docs/media-evidence/pr02-now-playing.png create mode 100644 docs/media-evidence/pr02-visual.md diff --git a/docs/media-evidence/pr02-now-playing-paused.png b/docs/media-evidence/pr02-now-playing-paused.png new file mode 100644 index 0000000000000000000000000000000000000000..f14ffec8a039817e59939302cb701305806efb96 GIT binary patch literal 49256 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 literal 0 HcmV?d00001 diff --git a/docs/media-evidence/pr02-now-playing-track-change.png b/docs/media-evidence/pr02-now-playing-track-change.png new file mode 100644 index 0000000000000000000000000000000000000000..2274a5280b99a174ad02a7c9c25e6cdd07ccc067 GIT binary patch literal 49237 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 #include #include +#include #include #include #include @@ -20,6 +21,7 @@ #include #include #include +#include struct WeaverAudioCapture { IMMDeviceEnumerator *enumerator = nullptr; @@ -156,6 +158,93 @@ extern "C" int weaver_audio_poll(WeaverAudioCapture *state, float *mono, size_t } constexpr size_t max_artwork_bytes = 1024 * 1024; +constexpr uint64_t max_artwork_pixel_bytes = 256 * 1024; + +static bool normalized_artwork_bytes( + const uint8_t *source, + size_t source_length, + std::vector &output) { + try { + winrt::com_ptr factory; + winrt::check_hresult(CoCreateInstance( + CLSID_WICImagingFactory, + nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(factory.put()))); + winrt::com_ptr input; + winrt::check_hresult(factory->CreateStream(input.put())); + winrt::check_hresult(input->InitializeFromMemory( + const_cast(source), + static_cast(source_length))); + winrt::com_ptr decoder; + winrt::check_hresult(factory->CreateDecoderFromStream( + input.get(), + nullptr, + WICDecodeMetadataCacheOnLoad, + decoder.put())); + winrt::com_ptr frame; + winrt::check_hresult(decoder->GetFrame(0, frame.put())); + UINT width = 0; + UINT height = 0; + winrt::check_hresult(frame->GetSize(&width, &height)); + if (width == 0 || height == 0) return false; + if (static_cast(width) * height * 4 <= max_artwork_pixel_bytes) { + output.assign(source, source + source_length); + return true; + } + + const double scale = 256.0 / static_cast(std::max(width, height)); + const UINT target_width = std::max(1, static_cast(width * scale)); + const UINT target_height = std::max(1, static_cast(height * scale)); + winrt::com_ptr scaler; + winrt::check_hresult(factory->CreateBitmapScaler(scaler.put())); + winrt::check_hresult(scaler->Initialize( + frame.get(), + target_width, + target_height, + WICBitmapInterpolationModeFant)); + winrt::com_ptr converter; + winrt::check_hresult(factory->CreateFormatConverter(converter.put())); + winrt::check_hresult(converter->Initialize( + scaler.get(), + GUID_WICPixelFormat32bppBGRA, + WICBitmapDitherTypeNone, + nullptr, + 0.0, + WICBitmapPaletteTypeCustom)); + + winrt::com_ptr output_stream; + winrt::check_hresult(CreateStreamOnHGlobal(nullptr, TRUE, output_stream.put())); + winrt::com_ptr encoder; + winrt::check_hresult(factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.put())); + winrt::check_hresult(encoder->Initialize(output_stream.get(), WICBitmapEncoderNoCache)); + winrt::com_ptr output_frame; + winrt::com_ptr properties; + winrt::check_hresult(encoder->CreateNewFrame(output_frame.put(), properties.put())); + winrt::check_hresult(output_frame->Initialize(properties.get())); + winrt::check_hresult(output_frame->SetSize(target_width, target_height)); + WICPixelFormatGUID pixel_format = GUID_WICPixelFormat32bppBGRA; + winrt::check_hresult(output_frame->SetPixelFormat(&pixel_format)); + if (pixel_format != GUID_WICPixelFormat32bppBGRA) return false; + winrt::check_hresult(output_frame->WriteSource(converter.get(), nullptr)); + winrt::check_hresult(output_frame->Commit()); + winrt::check_hresult(encoder->Commit()); + + HGLOBAL memory = nullptr; + winrt::check_hresult(GetHGlobalFromStream(output_stream.get(), &memory)); + const SIZE_T length = GlobalSize(memory); + if (length == 0 || length > max_artwork_bytes) return false; + const void *bytes = GlobalLock(memory); + if (!bytes) return false; + output.assign( + static_cast(bytes), + static_cast(bytes) + length); + GlobalUnlock(memory); + return true; + } catch (...) { + return false; + } +} struct WeaverMediaDirtyFlags { std::atomic session{true}; @@ -283,11 +372,15 @@ static bool read_thumbnail( winrt::Windows::Storage::Streams::DataReader reader(input); const uint32_t loaded = reader.LoadAsync(static_cast(size)).get(); if (loaded != size) return false; - auto bytes = std::unique_ptr(new (std::nothrow) uint8_t[loaded]); + std::vector raw(loaded); + reader.ReadBytes(winrt::array_view(raw)); + std::vector normalized; + if (!normalized_artwork_bytes(raw.data(), raw.size(), normalized)) return false; + auto bytes = std::unique_ptr(new (std::nothrow) uint8_t[normalized.size()]); if (!bytes) return false; - reader.ReadBytes(winrt::array_view(bytes.get(), bytes.get() + loaded)); + std::memcpy(bytes.get(), normalized.data(), normalized.size()); artwork->bytes = bytes.release(); - artwork->length = loaded; + artwork->length = normalized.size(); return true; } catch (...) { return false; diff --git a/sdk/CONTRACT.md b/sdk/CONTRACT.md index 4647b51c..5c6400a1 100644 --- a/sdk/CONTRACT.md +++ b/sdk/CONTRACT.md @@ -618,6 +618,13 @@ complete successfully published file can enter a provider frame. Successful publication updates the LRU timestamp; pruning retains at most 32 images and never removes the currently published hash. +The widget profile reserves 256 KiB of decoded RGBA per registered image. +Host artwork whose decoded dimensions exceed that fixed slot is aspect-fit +downsampled with Windows Imaging Component to at most 256×256 and encoded as +PNG before publication. The 1 MiB limit applies to both the original SMTC +payload and the normalized cache file. This transformation never expands a +runtime memory bound or touches the Native SDK dependency. + SMTC manager/session property events only set coalescing dirty flags. The existing one-second host poll consumes those flags, refreshes metadata and art, and continues to sample playback/timeline state. Event callbacks never perform From a2775043c7941b83ba2966fa9cf78445573f2025 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Sat, 25 Jul 2026 11:28:48 -0700 Subject: [PATCH 03/10] test: close album art cache and perf gates --- docs/media-evidence/pr02-visual.md | 23 +++++++++++++++++++++-- docs/media-run-status.md | 10 +++++++--- host/src/art_cache.zig | 17 +++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/docs/media-evidence/pr02-visual.md b/docs/media-evidence/pr02-visual.md index c93a979b..14b5b9aa 100644 --- a/docs/media-evidence/pr02-visual.md +++ b/docs/media-evidence/pr02-visual.md @@ -1,7 +1,9 @@ # Media PR 02 visual and live evidence -Date: 2026-07-25 -Machine: Windows 11, 2560×1440 primary display, Spotify Premium +Date: 2026-07-25 + +Machine: Windows 11, 2560×1440 primary display, Spotify Premium + Artifact: `node cli/bin/weaver.js dev examples/now-playing` The widget was allowed to settle on the real desktop layer. Other windows were @@ -72,3 +74,20 @@ the provider was allowed two seconds to poll. | Progress | yes | bottom row | purple/slate segments | retained paused position | PASS | Overall: **PASS**. + +## Static-image idle A/B + +The identical `examples/styling-images` source ran from ReleaseFast host and +runtime builds on the stack parent and layer-02 head. Each process settled for +at least 40 seconds before a 60-second `TotalProcessorTime` sample. No source +change occurred during either window. + +| Head | Settle | Sample | CPU start→end | CPU delta | Private start→end | +|---|---:|---:|---:|---:|---:| +| `media/01-status-sourceapp@2feb700` | 41 s | 60,014.772 ms | 125.000→125.000 ms | 0.000 ms | 42.719→42.684 MiB | +| `media/02-album-art@8f66767` | 42 s | 60,013.804 ms | 109.375→109.375 ms | 0.000 ms | 42.758→41.570 MiB | + +Claim: **no observed static-image idle CPU regression**. Both heads advanced +by zero Windows CPU accounting quanta over matched 60-second windows. The +layer-02 end-of-window private working allocation was 1.114 MiB lower in this +sample; this is recorded as an observation, not a generalized memory claim. diff --git a/docs/media-run-status.md b/docs/media-run-status.md index e96ee224..50eeca35 100644 --- a/docs/media-run-status.md +++ b/docs/media-run-status.md @@ -44,6 +44,10 @@ This run will not change the submodule pointer. `ImageTooLarge` failure with bounded host-side normalization. Viewed settled, track-change, and pause captures plus per-element results are in `docs/media-evidence/pr02-visual.md`. +- Layer 02 static-image ReleaseFast A/B: PASS. Parent and layer 02 each used + 0.000 ms process CPU over matched ~60.014 s windows after 41–42 s settles. + Private memory ended at 42.684 MiB parent / 41.570 MiB layer 02. Raw values + and the bounded claim are recorded with the visual evidence. ## Blockers and unverified gates @@ -58,9 +62,9 @@ Layer 02 implementation is in progress. The host now consumes coalesced SMTC events, publishes bounded artwork through the host-owned hash cache, and emits optional `artPath`; the runtime has canonical art-root containment and dynamic same-ID image re-registration. Automated and live visual gates pass; the -required static-image parent/head idle A/B is active. +required static-image parent/head idle A/B also passes. ## Next executable task -Measure parent/head idle CPU and private memory with the identical -`examples/styling-images` widget, then finalize the layer-02 draft PR. +Rerun the final layer-02 green matrix at the evidence head, open the draft PR +against layer 01, then branch layer 03 for duplex transport. diff --git a/host/src/art_cache.zig b/host/src/art_cache.zig index 54791fab..17bca108 100644 --- a/host/src/art_cache.zig +++ b/host/src/art_cache.zig @@ -240,5 +240,22 @@ test "art cache prunes to 32 files without pruning the published hash" { published = (try cache.publish(&payload)).?; } try std.testing.expectEqual(@as(usize, max_files), try testImageCount(std.testing.io, root)); + + // Make the pinned publication the oldest file, then force a prune without + // changing the published hash. Age must never outrank the pin. + { + var file = try std.Io.Dir.cwd().openFile(std.testing.io, published.pathSlice(), .{ .mode = .write_only }); + defer file.close(std.testing.io); + try file.setTimestamps(std.testing.io, .{ + .access_timestamp = .{ .new = .zero }, + .modify_timestamp = .{ .new = .zero }, + }); + } + const extra_name = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.img"; + const extra_path = try std.fs.path.join(std.testing.allocator, &.{ root, extra_name }); + defer std.testing.allocator.free(extra_path); + try std.Io.Dir.cwd().writeFile(std.testing.io, .{ .sub_path = extra_path, .data = "complete-extra" }); + try cache.prune(); + try std.testing.expectEqual(@as(usize, max_files), try testImageCount(std.testing.io, root)); _ = try std.Io.Dir.cwd().statFile(std.testing.io, published.pathSlice(), .{}); } From 84e1a46f0352d0d0d9c552383311c20d82c4cd00 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Sat, 25 Jul 2026 11:29:15 -0700 Subject: [PATCH 04/10] docs: record media layer 02 draft PR --- docs/media-run-status.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/media-run-status.md b/docs/media-run-status.md index 50eeca35..1e41d248 100644 --- a/docs/media-run-status.md +++ b/docs/media-run-status.md @@ -7,7 +7,7 @@ Last updated: 2026-07-25 | Layer | Branch | Parent | State | |---|---|---|---| | 01/05 | `media/01-status-sourceapp` | `master` (`b1199b5`) | DRAFT PR #32 (`2feb700`) | -| 02/05 | `media/02-album-art` | layer 01 | IN PROGRESS | +| 02/05 | `media/02-album-art` | layer 01 | DRAFT PR #33 (`bffe5c2`) | | 03/05 | `media/03-transport` | layer 02 | NOT STARTED | | 04/05 | `media/04-macos-adapter` | layer 03 | SPIKE-GATED / NOT STARTED | | 05/05 | `media/05-noro-gate` | layer 04 if proven, otherwise layer 03 | NOT STARTED | @@ -58,13 +58,12 @@ This run will not change the submodule pointer. ## Current work -Layer 02 implementation is in progress. The host now consumes coalesced SMTC -events, publishes bounded artwork through the host-owned hash cache, and emits -optional `artPath`; the runtime has canonical art-root containment and dynamic -same-ID image re-registration. Automated and live visual gates pass; the -required static-image parent/head idle A/B also passes. +Layer 02 is committed, pushed, and open as draft PR +`https://github.com/SunkenInTime/weaver/pull/33`. Automated, live visual, and +static-image idle A/B gates pass. ## Next executable task -Rerun the final layer-02 green matrix at the evidence head, open the draft PR -against layer 01, then branch layer 03 for duplex transport. +Create `media/03-transport` from layer 02 and implement the frozen duplex +channel, ack demultiplexing, verb semantics, capability gate, SDK transport +hook, and Windows live controls. From 2eecd49858665995db55adf9909175d7cff28159 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Sat, 25 Jul 2026 12:34:10 -0700 Subject: [PATCH 05/10] docs: keep album art contract implementation-neutral --- sdk/CONTRACT.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sdk/CONTRACT.md b/sdk/CONTRACT.md index 5c6400a1..6f641284 100644 --- a/sdk/CONTRACT.md +++ b/sdk/CONTRACT.md @@ -607,10 +607,10 @@ Widgets must use conditional rendering because `.src` remains required: : } ``` -weaverd is the cache's sole writer. On Windows the root is -`%LOCALAPPDATA%\weaver\artcache`, inheriting the per-user LocalAppData access -boundary; the runtime receives that one root in `WEAVER_ART_CACHE` only for -widgets subscribed to media. Filenames are lowercase SHA-256 hex plus `.img`. +weaverd is the cache's sole writer. On hosts with per-user LocalAppData, the +root is `%LOCALAPPDATA%\weaver\artcache`, inheriting that access boundary; the +runtime receives that one root in `WEAVER_ART_CACHE` only for widgets +subscribed to media. Filenames are lowercase SHA-256 hex plus `.img`. Inputs over 1 MiB are skipped. A new file is written to a unique exclusive temporary file, flushed and synchronized, then atomically renamed without replacement. Temporary files are removed on failure and startup. Only a @@ -620,8 +620,8 @@ never removes the currently published hash. The widget profile reserves 256 KiB of decoded RGBA per registered image. Host artwork whose decoded dimensions exceed that fixed slot is aspect-fit -downsampled with Windows Imaging Component to at most 256×256 and encoded as -PNG before publication. The 1 MiB limit applies to both the original SMTC +downsampled with the host image decoder to at most 256×256 and encoded as PNG +before publication. The 1 MiB limit applies to both the original provider payload and the normalized cache file. This transformation never expands a runtime memory bound or touches the Native SDK dependency. @@ -641,7 +641,7 @@ generation comparison. The runtime canonicalizes the widget root and host art-cache root once. Widget assets must remain within the widget root. Host art paths must be ordinary -drive-absolute Windows paths within the cache at a case-insensitive component -boundary. Drive-relative, rooted-current-drive, UNC, device-namespace, `..`, -missing, and prefix-collision paths are rejected. Widgets cannot nominate an -additional readable root. +drive-absolute paths within the cache at a case-insensitive component boundary. +Drive-relative, rooted-current-drive, UNC, device-namespace, `..`, missing, and +prefix-collision paths are rejected. Widgets cannot nominate an additional +readable root. From f6a6442d8f513af1eb8270cf6f84ac52348bb5a3 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Sat, 25 Jul 2026 17:40:58 -0700 Subject: [PATCH 06/10] fix: retain album art across refresh failures --- docs/media-run-status.md | 23 +++++++----- host/src/media.zig | 68 +++++++++++++++++++++++++--------- host/src/windows_providers.cpp | 12 ++++-- 3 files changed, 72 insertions(+), 31 deletions(-) diff --git a/docs/media-run-status.md b/docs/media-run-status.md index 1e41d248..089a41fc 100644 --- a/docs/media-run-status.md +++ b/docs/media-run-status.md @@ -7,10 +7,10 @@ Last updated: 2026-07-25 | Layer | Branch | Parent | State | |---|---|---|---| | 01/05 | `media/01-status-sourceapp` | `master` (`b1199b5`) | DRAFT PR #32 (`2feb700`) | -| 02/05 | `media/02-album-art` | layer 01 | DRAFT PR #33 (`bffe5c2`) | -| 03/05 | `media/03-transport` | layer 02 | NOT STARTED | -| 04/05 | `media/04-macos-adapter` | layer 03 | SPIKE-GATED / NOT STARTED | -| 05/05 | `media/05-noro-gate` | layer 04 if proven, otherwise layer 03 | NOT STARTED | +| 02/05 | `media/02-album-art` | layer 01 | DRAFT PR #33 (`2eecd49` plus adversarial repair pending push) | +| 03/05 | `media/03-transport` | layer 02 | DRAFT PR #34; restack pending | +| 04/05 | `media/04-macos-adapter` | layer 03 | DRAFT PR #36; restack pending | +| 05/05 | `media/05-noro-gate` | layer 04 | DRAFT PR #35; restack pending | The Native SDK submodule remains at `3f6a68b606e110087b5992cbe75f700051f1b7f3`. This run will not change the submodule pointer. @@ -48,6 +48,11 @@ This run will not change the submodule pointer. 0.000 ms process CPU over matched ~60.014 s windows after 41–42 s settles. Private memory ended at 42.684 MiB parent / 41.570 MiB layer 02. Raw values and the bounded claim are recorded with the visual evidence. +- Adversarial finding F9 repaired at its owning layer: transient thumbnail + refresh, normalization, size, and cache-publication failures retain the + prior art path and cache pin. Only a successful replacement or a confirmed + no-art state changes the snapshot. The new retention/no-art test and + `host: zig build test` pass. ## Blockers and unverified gates @@ -58,12 +63,10 @@ This run will not change the submodule pointer. ## Current work -Layer 02 is committed, pushed, and open as draft PR -`https://github.com/SunkenInTime/weaver/pull/33`. Automated, live visual, and -static-image idle A/B gates pass. +The 15-finding adversarial remediation is in progress bottom-up. F9 is fixed +and tested in layer 02; its complete per-layer gate and publication are next. ## Next executable task -Create `media/03-transport` from layer 02 and implement the frozen duplex -channel, ack demultiplexing, verb semantics, capability gate, SDK transport -hook, and Windows live controls. +Run the complete layer-02 gate, commit/push F9, then restack layer 03 and fix +F1/F2/F3/F4/F10/F11/F12 at the transport-owning layer. diff --git a/host/src/media.zig b/host/src/media.zig index 397a1eec..58598496 100644 --- a/host/src/media.zig +++ b/host/src/media.zig @@ -113,24 +113,7 @@ pub const Provider = struct { } else { self.refresh_failure_logged = false; } - if (artwork.changed != 0) { - self.current_art_path_len = 0; - if (self.cache) |cache| { - cache.clearPublished(); - if (artwork.too_large != 0) { - std.log.warn("SMTC thumbnail exceeds the 1 MiB art-cache limit; omitting art", .{}); - } else if (artwork.bytes != null and artwork.length > 0) { - const publication: ?art_cache.Publication = cache.publish(artwork.bytes[0..artwork.length]) catch |err| failure: { - std.log.err("media artwork cache publication failed: {s}", .{@errorName(err)}); - break :failure null; - }; - if (publication) |published| { - @memcpy(self.current_art_path[0..published.path_len], published.path[0..published.path_len]); - self.current_art_path_len = published.path_len; - } - } - } - } + self.applyArtwork(&artwork); if (result == 0) return .{}; var frame: Frame = .{ .status = statusFromNative(source.status), @@ -158,6 +141,29 @@ pub const Provider = struct { if (self.session) |session| native.weaver_media_destroy(session); self.session = null; } + + /// A transient refresh or publication failure must retain both the prior + /// path and the cache pin. The prior snapshot is cleared only when SMTC + /// successfully confirms that the current session has no artwork. + fn applyArtwork(self: *Provider, artwork: *const native.WeaverMediaArtwork) void { + if (artwork.changed == 0 or artwork.refresh_failed != 0) return; + if (artwork.too_large != 0) { + std.log.warn("SMTC thumbnail exceeds the 1 MiB art-cache limit; retaining prior art", .{}); + return; + } + if (artwork.bytes != null and artwork.length > 0) { + const cache = self.cache orelse return; + const publication = (cache.publish(artwork.bytes[0..artwork.length]) catch |err| { + std.log.err("media artwork cache publication failed; retaining prior art: {s}", .{@errorName(err)}); + return; + }) orelse return; + @memcpy(self.current_art_path[0..publication.path_len], publication.path[0..publication.path_len]); + self.current_art_path_len = publication.path_len; + return; + } + self.current_art_path_len = 0; + if (self.cache) |cache| cache.clearPublished(); + } }; pub fn formatFrame(frame: *const Frame, output: []u8) ![]const u8 { @@ -274,3 +280,29 @@ test "native playback status maps to the frozen tri-state" { test "native media dirty flags start dirty and coalesce duplicate events" { try std.testing.expectEqual(@as(c_int, 1), native.weaver_media_test_dirty_coalescing()); } + +test "art refresh failure retains prior path and cache pin until genuine no-art" { + const root = ".zig-cache/weaver-media-art-refresh-retention"; + std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + defer std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + var cache = try art_cache.Cache.init(std.testing.io, std.testing.allocator, root); + defer cache.deinit(); + const initial = (try cache.publish("prior-art")).?; + var provider: Provider = .{ .cache = &cache }; + @memcpy(provider.current_art_path[0..initial.path_len], initial.path[0..initial.path_len]); + provider.current_art_path_len = initial.path_len; + + var failed: native.WeaverMediaArtwork = std.mem.zeroes(native.WeaverMediaArtwork); + failed.changed = 1; + failed.refresh_failed = 1; + provider.applyArtwork(&failed); + try std.testing.expectEqualStrings(initial.pathSlice(), provider.current_art_path[0..provider.current_art_path_len]); + try std.testing.expect(cache.published); + try std.testing.expectEqualSlices(u8, &initial.hash, &cache.published_hash); + + var no_art: native.WeaverMediaArtwork = std.mem.zeroes(native.WeaverMediaArtwork); + no_art.changed = 1; + provider.applyArtwork(&no_art); + try std.testing.expectEqual(@as(usize, 0), provider.current_art_path_len); + try std.testing.expect(!cache.published); +} diff --git a/host/src/windows_providers.cpp b/host/src/windows_providers.cpp index d22d3e45..7cdcfda6 100644 --- a/host/src/windows_providers.cpp +++ b/host/src/windows_providers.cpp @@ -358,16 +358,21 @@ static void rebind_current_session(WeaverMediaSession *state) { static bool read_thumbnail( const winrt::Windows::Storage::Streams::IRandomAccessStreamReference &reference, WeaverMediaArtwork *artwork) { - artwork->changed = 1; - if (!reference) return true; + if (!reference) { + artwork->changed = 1; + return true; + } try { const auto stream = reference.OpenReadAsync().get(); const uint64_t size = stream.Size(); if (size > max_artwork_bytes) { artwork->too_large = 1; + return false; + } + if (size == 0) { + artwork->changed = 1; return true; } - if (size == 0) return true; const auto input = stream.GetInputStreamAt(0); winrt::Windows::Storage::Streams::DataReader reader(input); const uint32_t loaded = reader.LoadAsync(static_cast(size)).get(); @@ -381,6 +386,7 @@ static bool read_thumbnail( std::memcpy(bytes.get(), normalized.data(), normalized.size()); artwork->bytes = bytes.release(); artwork->length = normalized.size(); + artwork->changed = 1; return true; } catch (...) { return false; From 03c905626bb24c4bb6686a698ddd8410fe6d630a Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Sat, 25 Jul 2026 23:33:24 -0700 Subject: [PATCH 07/10] fix: isolate artwork across media sessions --- host/src/media.zig | 71 ++++++++++++++++++++++++++++++++-- host/src/windows_providers.cpp | 1 + host/src/windows_providers.h | 1 + 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/host/src/media.zig b/host/src/media.zig index 58598496..be9f1ddf 100644 --- a/host/src/media.zig +++ b/host/src/media.zig @@ -72,6 +72,8 @@ pub const Provider = struct { next_poll_ms: u64 = 0, current_art_path: [max_art_path_bytes]u8 = @splat(0), current_art_path_len: usize = 0, + current_art_matches_session: bool = false, + awaiting_session_art_resolution: bool = false, refresh_failure_logged: bool = false, pub fn deinit(self: *Provider) void { @@ -114,6 +116,11 @@ pub const Provider = struct { self.refresh_failure_logged = false; } self.applyArtwork(&artwork); + // A replacement session is published atomically with its artwork + // outcome. Until that outcome is known, returning null preserves the + // prior complete frame instead of pairing old art with new metadata or + // flashing a blank image. + if (self.awaiting_session_art_resolution) return null; if (result == 0) return .{}; var frame: Frame = .{ .status = statusFromNative(source.status), @@ -124,7 +131,9 @@ pub const Provider = struct { copyText(&frame.artist, &frame.artist_len, std.mem.sliceTo(&source.artist, 0)); copyText(&frame.album, &frame.album_len, std.mem.sliceTo(&source.album, 0)); copyText(&frame.source_app, &frame.source_app_len, std.mem.sliceTo(&source.source_app, 0)); - copyText(&frame.art_path, &frame.art_path_len, self.current_art_path[0..self.current_art_path_len]); + if (self.current_art_matches_session) { + copyText(&frame.art_path, &frame.art_path_len, self.current_art_path[0..self.current_art_path_len]); + } return frame; } @@ -132,6 +141,7 @@ pub const Provider = struct { self.session = native.weaver_media_create(); if (self.session != null) { self.next_poll_ms = now_ms; + self.awaiting_session_art_resolution = true; return; } self.next_open_ms = now_ms + 1000; @@ -140,12 +150,22 @@ pub const Provider = struct { fn close(self: *Provider) void { if (self.session) |session| native.weaver_media_destroy(session); self.session = null; + // Keep the durable cache snapshot pinned across a transient provider + // failure. A subsequently opened session must resolve its own artwork + // before the host replaces the prior complete frame. + self.current_art_matches_session = false; + self.awaiting_session_art_resolution = true; } /// A transient refresh or publication failure must retain both the prior - /// path and the cache pin. The prior snapshot is cleared only when SMTC - /// successfully confirms that the current session has no artwork. + /// path and the cache pin. A session boundary makes that snapshot ineligible + /// for frames until the replacement publishes art or confirms no art. The + /// snapshot itself is cleared only on a successful no-art observation. fn applyArtwork(self: *Provider, artwork: *const native.WeaverMediaArtwork) void { + if (artwork.session_changed != 0) { + self.current_art_matches_session = false; + self.awaiting_session_art_resolution = true; + } if (artwork.changed == 0 or artwork.refresh_failed != 0) return; if (artwork.too_large != 0) { std.log.warn("SMTC thumbnail exceeds the 1 MiB art-cache limit; retaining prior art", .{}); @@ -159,9 +179,13 @@ pub const Provider = struct { }) orelse return; @memcpy(self.current_art_path[0..publication.path_len], publication.path[0..publication.path_len]); self.current_art_path_len = publication.path_len; + self.current_art_matches_session = true; + self.awaiting_session_art_resolution = false; return; } self.current_art_path_len = 0; + self.current_art_matches_session = true; + self.awaiting_session_art_resolution = false; if (self.cache) |cache| cache.clearPublished(); } }; @@ -291,12 +315,14 @@ test "art refresh failure retains prior path and cache pin until genuine no-art" var provider: Provider = .{ .cache = &cache }; @memcpy(provider.current_art_path[0..initial.path_len], initial.path[0..initial.path_len]); provider.current_art_path_len = initial.path_len; + provider.current_art_matches_session = true; var failed: native.WeaverMediaArtwork = std.mem.zeroes(native.WeaverMediaArtwork); failed.changed = 1; failed.refresh_failed = 1; provider.applyArtwork(&failed); try std.testing.expectEqualStrings(initial.pathSlice(), provider.current_art_path[0..provider.current_art_path_len]); + try std.testing.expect(provider.current_art_matches_session); try std.testing.expect(cache.published); try std.testing.expectEqualSlices(u8, &initial.hash, &cache.published_hash); @@ -304,5 +330,44 @@ test "art refresh failure retains prior path and cache pin until genuine no-art" no_art.changed = 1; provider.applyArtwork(&no_art); try std.testing.expectEqual(@as(usize, 0), provider.current_art_path_len); + try std.testing.expect(provider.current_art_matches_session); try std.testing.expect(!cache.published); } + +test "session replacement refresh failure suppresses prior art without unpinning it" { + const root = ".zig-cache/weaver-media-art-session-boundary"; + std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + defer std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + var cache = try art_cache.Cache.init(std.testing.io, std.testing.allocator, root); + defer cache.deinit(); + const initial = (try cache.publish("prior-session-art")).?; + var provider: Provider = .{ .cache = &cache }; + @memcpy(provider.current_art_path[0..initial.path_len], initial.path[0..initial.path_len]); + provider.current_art_path_len = initial.path_len; + provider.current_art_matches_session = true; + + var failed: native.WeaverMediaArtwork = std.mem.zeroes(native.WeaverMediaArtwork); + failed.changed = 1; + failed.refresh_failed = 1; + failed.session_changed = 1; + provider.applyArtwork(&failed); + + try std.testing.expect(!provider.current_art_matches_session); + try std.testing.expect(provider.awaiting_session_art_resolution); + try std.testing.expectEqualStrings(initial.pathSlice(), provider.current_art_path[0..provider.current_art_path_len]); + try std.testing.expect(cache.published); + try std.testing.expectEqualSlices(u8, &initial.hash, &cache.published_hash); + + var replacement: native.WeaverMediaArtwork = std.mem.zeroes(native.WeaverMediaArtwork); + replacement.changed = 1; + replacement.bytes = @constCast("replacement-art".ptr); + replacement.length = "replacement-art".len; + provider.applyArtwork(&replacement); + try std.testing.expect(provider.current_art_matches_session); + try std.testing.expect(!provider.awaiting_session_art_resolution); + try std.testing.expect(!std.mem.eql( + u8, + initial.pathSlice(), + provider.current_art_path[0..provider.current_art_path_len], + )); +} diff --git a/host/src/windows_providers.cpp b/host/src/windows_providers.cpp index 7cdcfda6..6b37f883 100644 --- a/host/src/windows_providers.cpp +++ b/host/src/windows_providers.cpp @@ -429,6 +429,7 @@ extern "C" int weaver_media_poll(WeaverMediaSession *state, WeaverMediaState *ou if (session_changed) { rebind_current_session(state); artwork->changed = 1; + artwork->session_changed = 1; } const auto session = state->current; if (!session) return 0; diff --git a/host/src/windows_providers.h b/host/src/windows_providers.h index 138f0f5c..7a0805d4 100644 --- a/host/src/windows_providers.h +++ b/host/src/windows_providers.h @@ -38,6 +38,7 @@ typedef struct WeaverMediaArtwork { int changed; int too_large; int refresh_failed; + int session_changed; } WeaverMediaArtwork; WeaverMediaSession *weaver_media_create(void); From fc7393f2fc488c64df6d52df673ca30c9698ab66 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Sun, 26 Jul 2026 00:07:11 -0700 Subject: [PATCH 08/10] fix: retry transient media artwork refresh --- host/src/media.zig | 4 ++++ host/src/windows_providers.cpp | 17 ++++++++++++++++- host/src/windows_providers.h | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/host/src/media.zig b/host/src/media.zig index be9f1ddf..3f43fafc 100644 --- a/host/src/media.zig +++ b/host/src/media.zig @@ -305,6 +305,10 @@ test "native media dirty flags start dirty and coalesce duplicate events" { try std.testing.expectEqual(@as(c_int, 1), native.weaver_media_test_dirty_coalescing()); } +test "native media retries a consumed transient refresh failure" { + try std.testing.expectEqual(@as(c_int, 1), native.weaver_media_test_refresh_retry()); +} + test "art refresh failure retains prior path and cache pin until genuine no-art" { const root = ".zig-cache/weaver-media-art-refresh-retention"; std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; diff --git a/host/src/windows_providers.cpp b/host/src/windows_providers.cpp index 6b37f883..c114c7c8 100644 --- a/host/src/windows_providers.cpp +++ b/host/src/windows_providers.cpp @@ -355,6 +355,13 @@ static void rebind_current_session(WeaverMediaSession *state) { state->dirty.mark_properties(); } +static bool should_refresh_properties(bool dirty, bool refresh_failed) { + // The host calls this provider at most once per second and only while a + // widget subscribes to media. Retrying here therefore recovers a consumed + // transient first-refresh failure without creating an idle poll path. + return dirty || refresh_failed; +} + static bool read_thumbnail( const winrt::Windows::Storage::Streams::IRandomAccessStreamReference &reference, WeaverMediaArtwork *artwork) { @@ -433,7 +440,8 @@ extern "C" int weaver_media_poll(WeaverMediaSession *state, WeaverMediaState *ou } const auto session = state->current; if (!session) return 0; - if (state->dirty.take_properties()) { + const bool properties_dirty = state->dirty.take_properties(); + if (should_refresh_properties(properties_dirty, state->refresh_failed)) { try { const auto properties = session.TryGetMediaPropertiesAsync().get(); state->title = winrt::to_string(properties.Title()); @@ -487,3 +495,10 @@ extern "C" int weaver_media_test_dirty_coalescing(void) { dirty.mark_properties(); return dirty.take_properties() && !dirty.take_properties(); } + +extern "C" int weaver_media_test_refresh_retry(void) { + if (should_refresh_properties(false, false)) return 0; + if (!should_refresh_properties(true, false)) return 0; + if (!should_refresh_properties(false, true)) return 0; + return should_refresh_properties(true, true) ? 1 : 0; +} diff --git a/host/src/windows_providers.h b/host/src/windows_providers.h index 7a0805d4..213d56d4 100644 --- a/host/src/windows_providers.h +++ b/host/src/windows_providers.h @@ -47,6 +47,7 @@ int weaver_media_poll(WeaverMediaSession *session, WeaverMediaState *state, Weav void weaver_media_artwork_release(WeaverMediaArtwork *artwork); void weaver_media_select_source_app(const char *raw_id, const char *resolved_name, char output[257]); int weaver_media_test_dirty_coalescing(void); +int weaver_media_test_refresh_retry(void); #ifdef __cplusplus } From a153e6cb8c33c78874943c82386bc4bd7b5d185c Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Sun, 26 Jul 2026 00:32:30 -0700 Subject: [PATCH 09/10] fix: bound unresolved artwork replacement --- host/src/media.zig | 74 ++++++++++++++++++++++++++++++---- host/src/windows_providers.cpp | 45 +++++++++++++++++++-- host/src/windows_providers.h | 2 + 3 files changed, 111 insertions(+), 10 deletions(-) diff --git a/host/src/media.zig b/host/src/media.zig index 3f43fafc..d30fcf23 100644 --- a/host/src/media.zig +++ b/host/src/media.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const art_cache = @import("art_cache.zig"); const native = @cImport({ @@ -166,17 +167,39 @@ pub const Provider = struct { self.current_art_matches_session = false; self.awaiting_session_art_resolution = true; } - if (artwork.changed == 0 or artwork.refresh_failed != 0) return; - if (artwork.too_large != 0) { - std.log.warn("SMTC thumbnail exceeds the 1 MiB art-cache limit; retaining prior art", .{}); + if (artwork.unavailable != 0) { + if (!builtin.is_test) { + if (artwork.too_large != 0) { + std.log.warn("SMTC thumbnail exceeds the 1 MiB art-cache limit; publishing metadata without stale art", .{}); + } else { + std.log.warn("SMTC artwork remains unavailable after three subscribed polls; publishing metadata without stale art", .{}); + } + } + // Keep the durable prior path and cache pin untouched, but do not + // associate them with refreshed metadata. A later successful retry + // can make artwork eligible again. + self.resolveArtworkUnavailable(); return; } + if (artwork.refresh_failed != 0) { + self.current_art_matches_session = false; + self.awaiting_session_art_resolution = true; + return; + } + if (artwork.changed == 0) return; if (artwork.bytes != null and artwork.length > 0) { - const cache = self.cache orelse return; + const cache = self.cache orelse { + self.resolveArtworkUnavailable(); + return; + }; const publication = (cache.publish(artwork.bytes[0..artwork.length]) catch |err| { - std.log.err("media artwork cache publication failed; retaining prior art: {s}", .{@errorName(err)}); + std.log.err("media artwork cache publication failed; publishing metadata without stale art: {s}", .{@errorName(err)}); + self.resolveArtworkUnavailable(); return; - }) orelse return; + }) orelse { + self.resolveArtworkUnavailable(); + return; + }; @memcpy(self.current_art_path[0..publication.path_len], publication.path[0..publication.path_len]); self.current_art_path_len = publication.path_len; self.current_art_matches_session = true; @@ -188,6 +211,13 @@ pub const Provider = struct { self.awaiting_session_art_resolution = false; if (self.cache) |cache| cache.clearPublished(); } + + fn resolveArtworkUnavailable(self: *Provider) void { + // Retain the durable prior path and pin for recovery/housekeeping, but + // make the refreshed metadata frame explicitly artless. + self.current_art_matches_session = false; + self.awaiting_session_art_resolution = false; + } }; pub fn formatFrame(frame: *const Frame, output: []u8) ![]const u8 { @@ -309,6 +339,10 @@ test "native media retries a consumed transient refresh failure" { try std.testing.expectEqual(@as(c_int, 1), native.weaver_media_test_refresh_retry()); } +test "native media bounds unresolved artwork before publishing metadata without it" { + try std.testing.expectEqual(@as(c_int, 1), native.weaver_media_test_refresh_failure_bound()); +} + test "art refresh failure retains prior path and cache pin until genuine no-art" { const root = ".zig-cache/weaver-media-art-refresh-retention"; std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; @@ -326,7 +360,8 @@ test "art refresh failure retains prior path and cache pin until genuine no-art" failed.refresh_failed = 1; provider.applyArtwork(&failed); try std.testing.expectEqualStrings(initial.pathSlice(), provider.current_art_path[0..provider.current_art_path_len]); - try std.testing.expect(provider.current_art_matches_session); + try std.testing.expect(!provider.current_art_matches_session); + try std.testing.expect(provider.awaiting_session_art_resolution); try std.testing.expect(cache.published); try std.testing.expectEqualSlices(u8, &initial.hash, &cache.published_hash); @@ -375,3 +410,28 @@ test "session replacement refresh failure suppresses prior art without unpinning provider.current_art_path[0..provider.current_art_path_len], )); } + +test "permanent artwork failure publishes metadata without stale art" { + const root = ".zig-cache/weaver-media-art-unavailable"; + std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + defer std.Io.Dir.cwd().deleteTree(std.testing.io, root) catch {}; + var cache = try art_cache.Cache.init(std.testing.io, std.testing.allocator, root); + defer cache.deinit(); + const initial = (try cache.publish("prior-art")).?; + var provider: Provider = .{ .cache = &cache }; + @memcpy(provider.current_art_path[0..initial.path_len], initial.path[0..initial.path_len]); + provider.current_art_path_len = initial.path_len; + provider.current_art_matches_session = true; + + var unavailable: native.WeaverMediaArtwork = std.mem.zeroes(native.WeaverMediaArtwork); + unavailable.changed = 1; + unavailable.refresh_failed = 1; + unavailable.unavailable = 1; + provider.applyArtwork(&unavailable); + + try std.testing.expect(!provider.current_art_matches_session); + try std.testing.expect(!provider.awaiting_session_art_resolution); + try std.testing.expectEqualStrings(initial.pathSlice(), provider.current_art_path[0..provider.current_art_path_len]); + try std.testing.expect(cache.published); + try std.testing.expectEqualSlices(u8, &initial.hash, &cache.published_hash); +} diff --git a/host/src/windows_providers.cpp b/host/src/windows_providers.cpp index c114c7c8..80ee2f37 100644 --- a/host/src/windows_providers.cpp +++ b/host/src/windows_providers.cpp @@ -159,6 +159,7 @@ extern "C" int weaver_audio_poll(WeaverAudioCapture *state, float *mono, size_t constexpr size_t max_artwork_bytes = 1024 * 1024; constexpr uint64_t max_artwork_pixel_bytes = 256 * 1024; +constexpr uint32_t max_artwork_refresh_attempts = 3; static bool normalized_artwork_bytes( const uint8_t *source, @@ -281,6 +282,7 @@ struct WeaverMediaSession { std::string album; std::string source_app; bool refresh_failed = false; + uint32_t refresh_failure_count = 0; bool apartment_initialized = false; ~WeaverMediaSession() { @@ -343,6 +345,8 @@ static void rebind_current_session(WeaverMediaSession *state) { state->properties_subscribed = false; state->current = nullptr; clear_cached_properties(state); + state->refresh_failed = false; + state->refresh_failure_count = 0; state->current = state->manager.GetCurrentSession(); if (state->current) { state->source_app = source_app_name(state->current.SourceAppUserModelId()); @@ -362,6 +366,14 @@ static bool should_refresh_properties(bool dirty, bool refresh_failed) { return dirty || refresh_failed; } +static uint32_t next_refresh_failure_count(uint32_t current) { + return std::min(current + 1, max_artwork_refresh_attempts); +} + +static bool refresh_retry_exhausted(uint32_t count) { + return count >= max_artwork_refresh_attempts; +} + static bool read_thumbnail( const winrt::Windows::Storage::Streams::IRandomAccessStreamReference &reference, WeaverMediaArtwork *artwork) { @@ -374,7 +386,9 @@ static bool read_thumbnail( const uint64_t size = stream.Size(); if (size > max_artwork_bytes) { artwork->too_large = 1; - return false; + artwork->unavailable = 1; + artwork->changed = 1; + return true; } if (size == 0) { artwork->changed = 1; @@ -442,14 +456,28 @@ extern "C" int weaver_media_poll(WeaverMediaSession *state, WeaverMediaState *ou if (!session) return 0; const bool properties_dirty = state->dirty.take_properties(); if (should_refresh_properties(properties_dirty, state->refresh_failed)) { + bool refresh_succeeded = false; try { const auto properties = session.TryGetMediaPropertiesAsync().get(); state->title = winrt::to_string(properties.Title()); state->artist = winrt::to_string(properties.Artist()); state->album = winrt::to_string(properties.AlbumTitle()); - state->refresh_failed = !read_thumbnail(properties.Thumbnail(), artwork); + refresh_succeeded = read_thumbnail(properties.Thumbnail(), artwork); } catch (...) { - state->refresh_failed = true; + refresh_succeeded = false; + } + state->refresh_failed = !refresh_succeeded; + if (refresh_succeeded) { + state->refresh_failure_count = 0; + } else { + state->refresh_failure_count = next_refresh_failure_count(state->refresh_failure_count); + if (refresh_retry_exhausted(state->refresh_failure_count)) { + // Resolve refreshed metadata without stale art after a + // bounded window. Later subscribed polls keep retrying, so + // a recovered source can still publish artwork. + artwork->unavailable = 1; + artwork->changed = 1; + } } } artwork->refresh_failed = state->refresh_failed ? 1 : 0; @@ -502,3 +530,14 @@ extern "C" int weaver_media_test_refresh_retry(void) { if (!should_refresh_properties(false, true)) return 0; return should_refresh_properties(true, true) ? 1 : 0; } + +extern "C" int weaver_media_test_refresh_failure_bound(void) { + uint32_t count = 0; + count = next_refresh_failure_count(count); + if (refresh_retry_exhausted(count)) return 0; + count = next_refresh_failure_count(count); + if (refresh_retry_exhausted(count)) return 0; + count = next_refresh_failure_count(count); + if (!refresh_retry_exhausted(count)) return 0; + return next_refresh_failure_count(count) == max_artwork_refresh_attempts ? 1 : 0; +} diff --git a/host/src/windows_providers.h b/host/src/windows_providers.h index 213d56d4..1692a279 100644 --- a/host/src/windows_providers.h +++ b/host/src/windows_providers.h @@ -39,6 +39,7 @@ typedef struct WeaverMediaArtwork { int too_large; int refresh_failed; int session_changed; + int unavailable; } WeaverMediaArtwork; WeaverMediaSession *weaver_media_create(void); @@ -48,6 +49,7 @@ void weaver_media_artwork_release(WeaverMediaArtwork *artwork); void weaver_media_select_source_app(const char *raw_id, const char *resolved_name, char output[257]); int weaver_media_test_dirty_coalescing(void); int weaver_media_test_refresh_retry(void); +int weaver_media_test_refresh_failure_bound(void); #ifdef __cplusplus } From e8748fb1000ed738867f8667ad0eacce9dc62e64 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Sun, 26 Jul 2026 10:04:05 -0700 Subject: [PATCH 10/10] fix: close album artwork review gaps --- docs/media-run-status.md | 8 ++-- host/src/art_cache.zig | 31 ++++++++++++++-- host/src/media.zig | 4 ++ host/src/windows_providers.cpp | 68 ++++++++++++++++++++++++++++------ host/src/windows_providers.h | 1 + runtime/src/main.zig | 45 ++++++++++++++++++++-- 6 files changed, 135 insertions(+), 22 deletions(-) diff --git a/docs/media-run-status.md b/docs/media-run-status.md index 089a41fc..77949684 100644 --- a/docs/media-run-status.md +++ b/docs/media-run-status.md @@ -63,10 +63,10 @@ This run will not change the submodule pointer. ## Current work -The 15-finding adversarial remediation is in progress bottom-up. F9 is fixed -and tested in layer 02; its complete per-layer gate and publication are next. +The 15-finding adversarial remediation is in progress bottom-up. Layer 02's F9 +repair is committed and published after its complete per-layer gate passed. ## Next executable task -Run the complete layer-02 gate, commit/push F9, then restack layer 03 and fix -F1/F2/F3/F4/F10/F11/F12 at the transport-owning layer. +Restack layer 03, then fix F1/F2/F3/F4/F10/F11/F12 at the +transport-owning layer. diff --git a/host/src/art_cache.zig b/host/src/art_cache.zig index 17bca108..40d39b90 100644 --- a/host/src/art_cache.zig +++ b/host/src/art_cache.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const windows = std.os.windows; @@ -105,9 +106,7 @@ pub const Cache = struct { defer file.close(self.io); try file.setTimestampsNow(self.io); } - self.published_hash = hash; - self.published = true; - try self.prune(); + self.finishPublication(hash); var result: Publication = .{ .hash = hash }; @memcpy(result.path[0..final_path.len], final_path); @@ -115,6 +114,16 @@ pub const Cache = struct { return result; } + fn finishPublication(self: *Cache, hash: [hash_hex_bytes]u8) void { + self.published_hash = hash; + self.published = true; + self.prune() catch |err| { + if (!builtin.is_test) { + std.log.warn("art cache prune failed after publication: {s}", .{@errorName(err)}); + } + }; + } + fn cleanupTemps(self: *Cache) !void { var directory = try std.Io.Dir.cwd().openDir(self.io, self.root, .{ .iterate = true }); defer directory.close(self.io); @@ -226,6 +235,22 @@ test "art cache skips oversized input and cleans startup temps" { try std.testing.expectEqual(@as(usize, 0), try testImageCount(std.testing.io, root)); } +test "art cache keeps a completed publication when post-publish pruning fails" { + const root = try std.testing.allocator.dupe(u8, ".zig-cache/weaver-art-cache-missing-prune-root"); + var cache: Cache = .{ + .io = std.testing.io, + .allocator = std.testing.allocator, + .root = root, + }; + defer cache.deinit(); + const hash: [hash_hex_bytes]u8 = @splat('a'); + + cache.finishPublication(hash); + + try std.testing.expect(cache.published); + try std.testing.expectEqualSlices(u8, &hash, &cache.published_hash); +} + test "art cache prunes to 32 files without pruning the published hash" { const root = try testRoot(std.testing.allocator, "prune"); defer std.testing.allocator.free(root); diff --git a/host/src/media.zig b/host/src/media.zig index d30fcf23..17b0c7a2 100644 --- a/host/src/media.zig +++ b/host/src/media.zig @@ -335,6 +335,10 @@ test "native media dirty flags start dirty and coalesce duplicate events" { try std.testing.expectEqual(@as(c_int, 1), native.weaver_media_test_dirty_coalescing()); } +test "native media event callbacks stop at the shared lifetime fence" { + try std.testing.expectEqual(@as(c_int, 1), native.weaver_media_test_event_lifetime()); +} + test "native media retries a consumed transient refresh failure" { try std.testing.expectEqual(@as(c_int, 1), native.weaver_media_test_refresh_retry()); } diff --git a/host/src/windows_providers.cpp b/host/src/windows_providers.cpp index 80ee2f37..c99d1c08 100644 --- a/host/src/windows_providers.cpp +++ b/host/src/windows_providers.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -268,6 +269,21 @@ struct WeaverMediaDirtyFlags { } }; +struct WeaverMediaEventState { + std::atomic active{true}; + WeaverMediaDirtyFlags dirty; +}; + +static void mark_session_if_alive(const std::weak_ptr &weak_events) { + const auto events = weak_events.lock(); + if (events && events->active.load(std::memory_order_acquire)) events->dirty.mark_session(); +} + +static void mark_properties_if_alive(const std::weak_ptr &weak_events) { + const auto events = weak_events.lock(); + if (events && events->active.load(std::memory_order_acquire)) events->dirty.mark_properties(); +} + struct WeaverMediaSession { winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager manager{nullptr}; winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSession current{nullptr}; @@ -275,8 +291,7 @@ struct WeaverMediaSession { winrt::event_token properties_token{}; bool manager_subscribed = false; bool properties_subscribed = false; - std::atomic shutting_down{false}; - WeaverMediaDirtyFlags dirty; + std::shared_ptr events = std::make_shared(); std::string title; std::string artist; std::string album; @@ -286,7 +301,7 @@ struct WeaverMediaSession { bool apartment_initialized = false; ~WeaverMediaSession() { - shutting_down.store(true, std::memory_order_release); + events->active.store(false, std::memory_order_release); try { if (properties_subscribed && current) current.MediaPropertiesChanged(properties_token); } catch (...) { @@ -350,13 +365,14 @@ static void rebind_current_session(WeaverMediaSession *state) { state->current = state->manager.GetCurrentSession(); if (state->current) { state->source_app = source_app_name(state->current.SourceAppUserModelId()); + const std::weak_ptr weak_events = state->events; state->properties_token = state->current.MediaPropertiesChanged( - [state](const auto &, const auto &) { - if (!state->shutting_down.load(std::memory_order_acquire)) state->dirty.mark_properties(); + [weak_events](const auto &, const auto &) { + mark_properties_if_alive(weak_events); }); state->properties_subscribed = true; } - state->dirty.mark_properties(); + state->events->dirty.mark_properties(); } static bool should_refresh_properties(bool dirty, bool refresh_failed) { @@ -382,7 +398,12 @@ static bool read_thumbnail( return true; } try { - const auto stream = reference.OpenReadAsync().get(); + auto open = reference.OpenReadAsync(); + if (open.wait_for(std::chrono::seconds(2)) != winrt::Windows::Foundation::AsyncStatus::Completed) { + open.Cancel(); + return false; + } + const auto stream = open.GetResults(); const uint64_t size = stream.Size(); if (size > max_artwork_bytes) { artwork->too_large = 1; @@ -396,7 +417,12 @@ static bool read_thumbnail( } const auto input = stream.GetInputStreamAt(0); winrt::Windows::Storage::Streams::DataReader reader(input); - const uint32_t loaded = reader.LoadAsync(static_cast(size)).get(); + auto load = reader.LoadAsync(static_cast(size)); + if (load.wait_for(std::chrono::seconds(2)) != winrt::Windows::Foundation::AsyncStatus::Completed) { + load.Cancel(); + return false; + } + const uint32_t loaded = load.GetResults(); if (loaded != size) return false; std::vector raw(loaded); reader.ReadBytes(winrt::array_view(raw)); @@ -428,9 +454,10 @@ extern "C" WeaverMediaSession *weaver_media_create(void) { winrt::init_apartment(winrt::apartment_type::multi_threaded); state->apartment_initialized = true; state->manager = winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get(); + const std::weak_ptr weak_events = state->events; state->manager_token = state->manager.CurrentSessionChanged( - [raw = state.get()](const auto &, const auto &) { - if (!raw->shutting_down.load(std::memory_order_acquire)) raw->dirty.mark_session(); + [weak_events](const auto &, const auto &) { + mark_session_if_alive(weak_events); }); state->manager_subscribed = true; return state.release(); @@ -446,7 +473,7 @@ extern "C" int weaver_media_poll(WeaverMediaSession *state, WeaverMediaState *ou std::memset(output, 0, sizeof(*output)); std::memset(artwork, 0, sizeof(*artwork)); try { - const bool session_changed = state->dirty.take_session(); + const bool session_changed = state->events->dirty.take_session(); if (session_changed) { rebind_current_session(state); artwork->changed = 1; @@ -454,7 +481,7 @@ extern "C" int weaver_media_poll(WeaverMediaSession *state, WeaverMediaState *ou } const auto session = state->current; if (!session) return 0; - const bool properties_dirty = state->dirty.take_properties(); + const bool properties_dirty = state->events->dirty.take_properties(); if (should_refresh_properties(properties_dirty, state->refresh_failed)) { bool refresh_succeeded = false; try { @@ -524,6 +551,23 @@ extern "C" int weaver_media_test_dirty_coalescing(void) { return dirty.take_properties() && !dirty.take_properties(); } +extern "C" int weaver_media_test_event_lifetime(void) { + auto events = std::make_shared(); + const std::weak_ptr weak_events = events; + if (!events->dirty.take_session() || !events->dirty.take_properties()) return 0; + mark_session_if_alive(weak_events); + mark_properties_if_alive(weak_events); + if (!events->dirty.take_session() || !events->dirty.take_properties()) return 0; + events->active.store(false, std::memory_order_release); + mark_session_if_alive(weak_events); + mark_properties_if_alive(weak_events); + if (events->dirty.take_session() || events->dirty.take_properties()) return 0; + events.reset(); + mark_session_if_alive(weak_events); + mark_properties_if_alive(weak_events); + return weak_events.expired() ? 1 : 0; +} + extern "C" int weaver_media_test_refresh_retry(void) { if (should_refresh_properties(false, false)) return 0; if (!should_refresh_properties(true, false)) return 0; diff --git a/host/src/windows_providers.h b/host/src/windows_providers.h index 1692a279..b602165d 100644 --- a/host/src/windows_providers.h +++ b/host/src/windows_providers.h @@ -48,6 +48,7 @@ int weaver_media_poll(WeaverMediaSession *session, WeaverMediaState *state, Weav void weaver_media_artwork_release(WeaverMediaArtwork *artwork); void weaver_media_select_source_app(const char *raw_id, const char *resolved_name, char output[257]); int weaver_media_test_dirty_coalescing(void); +int weaver_media_test_event_lifetime(void); int weaver_media_test_refresh_retry(void); int weaver_media_test_refresh_failure_bound(void); diff --git a/runtime/src/main.zig b/runtime/src/main.zig index b3d7e4b8..2c35fc00 100644 --- a/runtime/src/main.zig +++ b/runtime/src/main.zig @@ -66,6 +66,7 @@ fn bridgeTimerCapacity() usize { return @import("bridge.zig").max_timers; } const max_images: usize = 16; +const max_image_load_attempts: u8 = 3; const fetch_poll_key: u64 = 0x7766_6574_6368; const provider_poll_key: u64 = 0x7770_726f_7669; const geometry_save_key: u64 = 0x7767_656f_6d65; @@ -76,6 +77,7 @@ const ImageState = struct { epoch: u64 = 0, observed: ?[:0]u8 = null, observed_valid: bool = false, + load_failure_count: u8 = 0, registered: bool = false, }; @@ -852,6 +854,7 @@ fn replaceObserved(state: *ImageState, observed: [:0]u8, valid: bool) void { if (state.observed) |previous| std.heap.page_allocator.free(previous); state.observed = observed; state.observed_valid = valid; + state.load_failure_count = 0; } fn rememberRawSource(state: *ImageState, source: []const u8) !void { @@ -862,6 +865,17 @@ fn observedEquals(state: *const ImageState, source: []const u8, valid: bool) boo return state.observed_valid == valid and if (state.observed) |observed| std.mem.eql(u8, observed, source) else false; } +fn recordImageLoadFailure(state: *ImageState, observed: [:0]u8) void { + const prior_failure_count = if (observedEquals(state, observed, true)) state.load_failure_count else 0; + replaceObserved(state, observed, true); + state.load_failure_count = @min(prior_failure_count + 1, max_image_load_attempts); +} + +fn observedImageLoadSettled(state: *const ImageState, source: []const u8) bool { + if (!observedEquals(state, source, true)) return false; + return state.load_failure_count == 0 or state.load_failure_count >= max_image_load_attempts; +} + fn invalidImageSourceError(err: anyerror) bool { return switch (err) { error.InvalidImageSource, @@ -890,7 +904,7 @@ fn synchronizeImageNode(model: *Model, effects: *Effects, id: tree_mod.NodeId, n } return; }; - if (observedEquals(state, resolved.path, true)) { + if (observedImageLoadSettled(state, resolved.path)) { std.heap.page_allocator.free(resolved.path); return; } @@ -900,13 +914,13 @@ fn synchronizeImageNode(model: *Model, effects: *Effects, id: tree_mod.NodeId, n std.heap.page_allocator, .limited(1024 * 1024), ) catch |err| { - replaceObserved(state, resolved.path, true); + recordImageLoadFailure(state, resolved.path); std.log.err("image reload read failed; keeping the prior image: {s}", .{@errorName(err)}); return; }; defer std.heap.page_allocator.free(bytes); _ = effects.registerImageBytes(id, bytes) catch |err| { - replaceObserved(state, resolved.path, true); + recordImageLoadFailure(state, resolved.path); std.log.err("image reload decode/register failed; keeping the prior image: {s}", .{@errorName(err)}); return; }; @@ -914,6 +928,31 @@ fn synchronizeImageNode(model: *Model, effects: *Effects, id: tree_mod.NodeId, n state.registered = true; } +test "image reload failures retry twice then settle until the source changes" { + var state: ImageState = .{}; + defer if (state.observed) |observed| std.heap.page_allocator.free(observed); + + recordImageLoadFailure(&state, try std.heap.page_allocator.dupeZ(u8, "first.img")); + try std.testing.expect(!observedImageLoadSettled(&state, "first.img")); + try std.testing.expectEqual(@as(u8, 1), state.load_failure_count); + + recordImageLoadFailure(&state, try std.heap.page_allocator.dupeZ(u8, "first.img")); + try std.testing.expect(!observedImageLoadSettled(&state, "first.img")); + try std.testing.expectEqual(@as(u8, 2), state.load_failure_count); + + recordImageLoadFailure(&state, try std.heap.page_allocator.dupeZ(u8, "first.img")); + try std.testing.expect(observedImageLoadSettled(&state, "first.img")); + try std.testing.expectEqual(max_image_load_attempts, state.load_failure_count); + + recordImageLoadFailure(&state, try std.heap.page_allocator.dupeZ(u8, "second.img")); + try std.testing.expect(!observedImageLoadSettled(&state, "second.img")); + try std.testing.expectEqual(@as(u8, 1), state.load_failure_count); + + replaceObserved(&state, try std.heap.page_allocator.dupeZ(u8, "second.img"), true); + try std.testing.expect(observedImageLoadSettled(&state, "second.img")); + try std.testing.expectEqual(@as(u8, 0), state.load_failure_count); +} + /// Runs after every app-loop turn, but the generation equality is the static /// fast path. A changed image is decoded before its same-ID resource is /// replaced; invalid/dead/reused nodes are unregistered explicitly.