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