From a8dc5d18bb2a557aebe0f69ac6207b4976951b46 Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Fri, 9 Oct 2026 12:19:23 +0800 Subject: [PATCH] fix: show snow when no frontend owns the screen Idle video held black except behind the stock OSD, so a setup with no frontend and no OSD (kiosk mode, the idle screensaver's first stage, a quit frontend) lost its background. Main now reports through status[10] that no frontend owns or is about to take the screen, and the core shows snow for that as well as behind the OSD. The bit is clear at reset, so a frontend start stays black, and a Main that never sets it behaves as before. --- README.md | 7 +++- files.qip | 1 + menu.sv | 31 +++++++++------- rtl/zaparoo_bootstrap_video.sv | 3 +- rtl/zaparoo_snow_enable.sv | 31 ++++++++++++++++ tb/bootstrap_video_tb.sv | 6 +-- tb/run.sh | 2 + tb/snow_enable_tb.sv | 68 ++++++++++++++++++++++++++++++++++ 8 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 rtl/zaparoo_snow_enable.sv create mode 100644 tb/snow_enable_tb.sv diff --git a/README.md b/README.md index 808defb..935e4ab 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,11 @@ the core itself: 352x240p60 (NTSC) by default, with 720x480i60 and 352x288p50 (PAL) selectable by the ARM-side launcher through a DDR control block (see [DDR contract](rtl/native_video_reader.sv)). There are no video options in the OSD — the mode and the H/V centering trims are owned by the launcher. Idle video stays -black during frontend handoff; upstream-style snow appears behind the stock -OSD. Native frontend frames take priority over both backgrounds. +black during frontend handoff. Upstream-style snow appears behind the stock +OSD, and whenever Main sets `status[10]` to report that no frontend owns or +is about to take the screen (frontend off, quit, or given up), so a setup +with no frontend and no OSD, such as kiosk mode, still shows snow. Native +frontend frames take priority over both backgrounds. Native writers must use the v2 magic and publish a live frame counter. The legacy 320-pixel, no-magic DDR contract is no longer accepted. diff --git a/files.qip b/files.qip index fd608e5..97e9abc 100644 --- a/files.qip +++ b/files.qip @@ -13,3 +13,4 @@ set_global_assignment -name SYSTEMVERILOG_FILE sys/mister_magik_bootstrap_black. set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_bootstrap_video.sv set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_snow_phase.sv set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_pixel_enable.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_snow_enable.sv diff --git a/menu.sv b/menu.sv index 5cbcfdb..93b032b 100644 --- a/menu.sv +++ b/menu.sv @@ -320,6 +320,8 @@ end wire FB = status[5]; wire [2:0] led = status[8:6]; +// Set by Main while no frontend owns or is about to take the screen. +wire snow_allowed = status[10]; // Pixel clock: CLK_VIDEO = 27.000 MHz (the universal SD video clock). // ce_pix /4 = 6.75 MHz gives exactly 15734.27 Hz (NTSC, 429-px line) and @@ -402,26 +404,27 @@ zaparoo_snow_phase snow_motion ( .new_frame(native_new_frame), .vcount(native_vcount), .phase(snow_phase) ); -// OSD status comes from the HDMI domain. Main disables OSD before handing -// video to the frontend, so startup stays black without a new bus command. -(* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED_IF_ASYNCHRONOUS" *) -reg [1:0] snow_osd_sync = 0; always @(posedge CLK_VIDEO) begin - if (RESET | ~vid_locked) begin - snow_sample <= 0; - snow_osd_sync <= 0; - end else begin - if (ce_pix) snow_sample <= snow_random[2:0]; - snow_osd_sync <= {snow_osd_sync[0], OSD_STATUS}; - end + if (RESET | ~vid_locked) snow_sample <= 0; + else if (ce_pix) snow_sample <= snow_random[2:0]; end -// Black handoff, snow behind stock OSD, or native frontend RGB. All three -// share the same native sync/DE; no video-mode switch is needed. +// Snow shows behind the stock OSD, and whenever Main reports that no frontend +// will draw. Main disables the OSD and leaves status[10] clear before handing +// video to the frontend, so that startup stays black. +wire show_snow; +zaparoo_snow_enable snow_enable ( + .clk(CLK_VIDEO), .reset(RESET | ~vid_locked), + .osd_status(OSD_STATUS), .snow_allowed(snow_allowed), + .show_snow(show_snow) +); + +// Black handoff, snow, or native frontend RGB. All three share the same +// native sync/DE; no video-mode switch is needed. zaparoo_bootstrap_video bootstrap_video ( .native_active(native_active), .native_rgb({native_r, native_g, native_b}), - .show_snow(snow_osd_sync[1]), + .show_snow(show_snow), .snow_rgb({snow_pixel, snow_pixel, snow_pixel}), .de_in(native_de), .hs_in(native_hs), .vs_in(native_vs), .rgb_out({VGA_R, VGA_G, VGA_B}), diff --git a/rtl/zaparoo_bootstrap_video.sv b/rtl/zaparoo_bootstrap_video.sv index 117137f..d3357fd 100644 --- a/rtl/zaparoo_bootstrap_video.sv +++ b/rtl/zaparoo_bootstrap_video.sv @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// Native CRT frames take priority; idle stays black except behind stock OSD. +// Native CRT frames take priority; idle is black unless show_snow is set +// (see zaparoo_snow_enable). `timescale 1ns/1ps module zaparoo_bootstrap_video ( input wire native_active, diff --git a/rtl/zaparoo_snow_enable.sv b/rtl/zaparoo_snow_enable.sv new file mode 100644 index 0000000..3aa9c87 --- /dev/null +++ b/rtl/zaparoo_snow_enable.sv @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Decides when idle video shows snow instead of handoff black. Both inputs +// come from other clock domains and are synchronized here. +// osd_status: the stock OSD is up, which always sits on snow. +// snow_allowed: Main reports that no frontend owns or is about to take the +// screen. Clear at reset, so startup stays black until Main +// says otherwise, and under a Main that never sets it. +`timescale 1ns/1ps +module zaparoo_snow_enable ( + input wire clk, + input wire reset, + input wire osd_status, + input wire snow_allowed, + output wire show_snow +); + (* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED_IF_ASYNCHRONOUS" *) + reg [1:0] osd_sync = 0; + (* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED_IF_ASYNCHRONOUS" *) + reg [1:0] allowed_sync = 0; + always @(posedge clk) begin + if (reset) begin + osd_sync <= 0; + allowed_sync <= 0; + end + else begin + osd_sync <= {osd_sync[0], osd_status}; + allowed_sync <= {allowed_sync[0], snow_allowed}; + end + end + assign show_snow = osd_sync[1] | allowed_sync[1]; +endmodule diff --git a/tb/bootstrap_video_tb.sv b/tb/bootstrap_video_tb.sv index 3486fad..77a1093 100644 --- a/tb/bootstrap_video_tb.sv +++ b/tb/bootstrap_video_tb.sv @@ -29,14 +29,14 @@ module bootstrap_video_tb; native_active = 0; #1; assert(rgb_out == 0) else $fatal(1, "writer stop did not restore black"); show_snow = 1; de_in = 1; #1; - assert(rgb_out == snow_rgb) else $fatal(1, "OSD did not reveal snow"); + assert(rgb_out == snow_rgb) else $fatal(1, "show_snow did not reveal snow"); de_in = 0; #1; assert(rgb_out == 0) else $fatal(1, "snow painted outside DE"); native_active = 1; #1; assert(rgb_out == native_rgb) else $fatal(1, "snow covered native frontend"); native_active = 0; show_snow = 0; de_in = 1; #1; - assert(rgb_out == 0) else $fatal(1, "OSD close did not restore black"); - $display("PASS: black handoff, OSD snow, native CRT priority and timing"); + assert(rgb_out == 0) else $fatal(1, "clearing show_snow did not restore black"); + $display("PASS: black handoff, snow, native CRT priority and timing"); $finish; end endmodule diff --git a/tb/run.sh b/tb/run.sh index 337a1b3..015ac19 100755 --- a/tb/run.sh +++ b/tb/run.sh @@ -19,6 +19,8 @@ run_test bootstrap_video_tb \ run_test snow_phase_tb ../rtl/zaparoo_snow_phase.sv snow_phase_tb.sv +run_test snow_enable_tb ../rtl/zaparoo_snow_enable.sv snow_enable_tb.sv + run_test pixel_enable_tb ../rtl/zaparoo_pixel_enable.sv pixel_enable_tb.sv run_test scanout_tb \ diff --git a/tb/snow_enable_tb.sv b/tb/snow_enable_tb.sv new file mode 100644 index 0000000..05da2d6 --- /dev/null +++ b/tb/snow_enable_tb.sv @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +`timescale 1ns/1ps +module snow_enable_tb; + reg clk = 0; + always #5 clk = ~clk; + reg reset = 1; + reg osd_status = 0; + reg snow_allowed = 0; + wire show_snow; + zaparoo_snow_enable dut (.*); + + // Two synchronizer stages: an input change is visible after two edges. + task settle; + begin + repeat (2) @(posedge clk); + #1; + end + endtask + + initial begin + // Reset wins over both sources, so a video restart begins black. + osd_status = 1; snow_allowed = 1; + repeat (4) begin + @(posedge clk); #1; + assert(show_snow === 1'b0) else $fatal(1, "snow during reset"); + end + @(negedge clk); reset = 0; osd_status = 0; snow_allowed = 0; + settle; + assert(show_snow === 1'b0) else $fatal(1, "idle did not start black"); + + // Every combination: either source reveals snow, neither keeps black. + for (integer i = 0; i < 4; i = i + 1) begin + @(negedge clk); {osd_status, snow_allowed} = i[1:0]; + @(posedge clk); #1; + settle; + assert(show_snow === (osd_status | snow_allowed)) + else $fatal(1, "snow source mismatch: osd=%0d allowed=%0d", osd_status, snow_allowed); + end + + // No frontend and no OSD (kiosk): snow stays up across an OSD cycle. + @(negedge clk); osd_status = 0; snow_allowed = 1; + settle; + assert(show_snow === 1'b1) else $fatal(1, "no snow without frontend or OSD"); + @(negedge clk); osd_status = 1; + settle; + @(negedge clk); osd_status = 0; + repeat (4) begin + @(posedge clk); #1; + assert(show_snow === 1'b1) else $fatal(1, "OSD close dropped allowed snow"); + end + + // A frontend taking the screen returns to black within the sync depth. + @(negedge clk); snow_allowed = 0; + settle; + assert(show_snow === 1'b0) else $fatal(1, "frontend takeover did not restore black"); + + // One edge after a change the output still holds: no path skips a stage. + @(negedge clk); snow_allowed = 1; + @(posedge clk); #1; + assert(show_snow === 1'b0) else $fatal(1, "snow bypassed the synchronizer"); + + $display("PASS: snow enable reset, OSD and no-frontend sources, takeover black"); + $finish; + end + initial begin + #20000; $fatal(1, "snow enable test timeout"); + end +endmodule