blob: 2ad732bdb5325e5f9208f21a8d554475ac604acc [file] [log] [blame]
// vdp.v
//
// Copyright (C) 2020 Dan Rodrigues <danrr.gh.oss@gmail.com>
//
// SPDX-License-Identifier: Apache-2.0
`default_nettype none
`include "layer_encoding.vh"
module vdp #(
parameter INIT_PALETTE_RAM = "",
parameter INIT_X_BLOCK = "",
parameter INIT_Y_BLOCK = "",
parameter INIT_G_BLOCK = "",
parameter integer CLIP_LEFT = 64,
parameter integer CLIP_RIGHT = 64,
parameter [0:0] ENABLE_WIDESCREEN = 0,
parameter [1:0] LAYERS_TOTAL = 3
) (
input clk,
input reset,
// Host interface
input [15:0] host_address,
input [15:0] host_write_data,
input host_write_en,
input host_read_en,
output reg [15:0] host_read_data,
output host_ready,
// VGA output
output reg [3:0] r,
output reg [3:0] g,
output reg [3:0] b,
output vga_hsync,
output vga_vsync,
output active_display,
input hold_raster,
// Single-cycle strobes, NOT suitable for VGA
output line_ended,
output frame_ended,
output active_frame_ended,
// VRAM interface
output [13:0] vram_address,
output vram_we_even,
input [15:0] vram_read_data_even,
output [15:0] vram_write_data_even,
output vram_we_odd,
input [15:0] vram_read_data_odd,
output [15:0] vram_write_data_odd,
// Copper RAM interface
output cop_ram_read_en,
output [10:0] cop_ram_read_address,
input [15:0] cop_ram_read_data
);
// --- Video timing ---
// 848x480@60hz
localparam H_ACTIVE_WIDTH_848 = 848;
localparam H_SYNC_848 = 112;
localparam H_BACKPORCH_848 = 112;
localparam V_FRONTPORCH_848 = 6;
localparam V_SYNC_848 = 8;
localparam V_BACKPORCH_848 = 23;
// 640x480@60hz
localparam H_ACTIVE_WIDTH_640 = 640;
localparam H_SYNC_640 = 96;
localparam H_BACKPORCH_640 = 48;
localparam V_FRONTPORCH_640 = 11;
localparam V_SYNC_640 = 2;
localparam V_BACKPORCH_640 = 31;
// common between both modes
localparam V_ACTIVE_HEIGHT = 480;
localparam H_FRONTPORCH = 16;
// select one of the above video modes according to parameter
localparam H_ACTIVE_WIDTH = ENABLE_WIDESCREEN ? H_ACTIVE_WIDTH_848 : H_ACTIVE_WIDTH_640;
localparam H_SYNC = ENABLE_WIDESCREEN ? H_SYNC_848 : H_SYNC_640;
localparam H_BACKPORCH = ENABLE_WIDESCREEN ? H_BACKPORCH_848 : H_BACKPORCH_640;
localparam V_FRONTPORCH = ENABLE_WIDESCREEN ? V_FRONTPORCH_848 : V_FRONTPORCH_640;
localparam V_SYNC = ENABLE_WIDESCREEN ? V_SYNC_848 : V_SYNC_640;
localparam V_BACKPORCH = ENABLE_WIDESCREEN ? V_BACKPORCH_848 : V_BACKPORCH_640;
// for use by other blocks below
localparam OFFSCREEN_X_TOTAL = H_SYNC + H_FRONTPORCH + H_BACKPORCH;
localparam HEIGHT_TOTAL = V_ACTIVE_HEIGHT + V_FRONTPORCH + V_SYNC + V_BACKPORCH;
wire [10:0] raster_x;
wire [9:0] raster_y;
// verilator lint_off PINMISSING
vdp_vga_timing #(
.H_ACTIVE_WIDTH(H_ACTIVE_WIDTH),
.V_ACTIVE_HEIGHT(V_ACTIVE_HEIGHT),
.H_FRONTPORCH(H_FRONTPORCH),
.H_SYNC(H_SYNC),
.H_BACKPORCH(H_BACKPORCH),
.V_FRONTPORCH(V_FRONTPORCH),
.V_SYNC(V_SYNC),
.V_BACKPORCH(V_BACKPORCH)
) vga_timing (
.clk(clk),
.reset(reset),
.hold_raster(hold_raster),
.raster_x(raster_x),
.raster_y(raster_y),
.hsync(vga_hsync),
.vsync(vga_vsync),
.active_display(active_display),
.active_frame_ended(active_frame_ended),
.line_ended(line_ended),
.frame_ended(frame_ended)
);
// verilator lint_on PINMISSING
// --- Host interface ---
wire [4:0] register_write_address, register_read_address;
wire [15:0] register_write_data;
wire register_write_en;
vdp_host_interface host_interface(
.clk(clk),
.reset(reset),
.host_read_en(host_read_en),
.ready(host_ready),
.vram_write_pending(vram_write_pending),
.host_write_en(host_write_en),
.register_write_en(register_write_en),
.cop_write_address(5'b0),
.cop_write_data(16'b0),
.cop_write_en(1'b0),
.host_address(host_address[5:0]),
.host_write_data(host_write_data),
.register_write_data(register_write_data),
.register_write_address(register_write_address),
.read_address(register_read_address)
);
// --- Copper ---
// (vdp_lite doesn't have the copper coprocessor)
// --- Register writes ---
reg [5:0] layer_enable = 0;
reg [4:0] layer_enable_alpha_over = 0;
wire affine_enabled = 0;
reg [15:0] scroll_tile_base;
reg [15:0] scroll_map_base;
reg [15:0] scroll_x [0:3];
reg [15:0] scroll_y [0:3];
reg [3:0] scroll_use_wide_map;
reg [3:0] sprite_tile_base;
wire [13:0] full_sprite_tile_base = {sprite_tile_base, 10'b0};
reg [15:0] vram_write_data_16b;
reg [1:0] vram_port_write_en_mask;
reg [14:0] vram_write_address_full;
wire [13:0] vram_write_address_16b = vram_write_address_full[14:1];
reg [7:0] vram_port_address_increment;
reg vram_write_pending;
// --- Writes: comb. ---
always @* begin
palette_write_en = 0;
// This block has the same structure as the below one, for consistency
// Looks odd while only one case is handled for the time being
if (register_write_en) begin
if (!register_write_address[4]) begin
case (register_write_address[3:0])
3: begin
palette_write_en = 1;
end
endcase
end
end
end
// --- Writes: clocked ---
always @(posedge clk) begin
if (reset) begin
layer_enable <= `LAYER_SPRITES_OHE;
layer_enable_alpha_over <= 0;
sprite_tile_base <= 0;
vram_write_pending <= 0;
end
sprite_metadata_write_en <= 0;
sprite_metadata_block_select <= sprite_metadata_block_select_nx;
if (sprite_metadata_write_en && sprite_meta_address_needs_increment) begin
sprite_metadata_address <= sprite_metadata_address + 1;
end
if (palette_write_en) begin
palette_write_address <= palette_write_address + 1;
end
if (vram_write_pending && vram_written) begin
vram_write_pending <= 0;
vram_write_address_full <= vram_write_address_full + vram_port_address_increment;
vram_port_write_en_mask <= 2'b00;
end
if (register_write_en) begin
if (!register_write_address[4]) begin
case (register_write_address[3:0])
0: begin
sprite_metadata_address <= register_write_data[7:0];
sprite_metadata_block_select <= 3'b001;
end
1: begin
// (sprite_metadata_write_data driven directly by register_write_data below)
sprite_metadata_write_en <= 1;
end
2: begin
palette_write_address <= register_write_data[1:0];
end
3: begin
// (palette write, which is handed separately above)
end
4: begin
vram_write_address_full <= register_write_data;
vram_port_write_en_mask <= 2'b00;
end
5: begin
vram_write_data_16b <= register_write_data;
vram_port_write_en_mask <= vram_write_address_full[0] ? 2'b10 : 2'b01;
vram_write_pending <= 1;
end
6: begin
vram_port_address_increment <= register_write_data[7:0];
end
7: begin
sprite_tile_base <= register_write_data[13:10];
end
8: begin
// cop_enable <= register_write_data[0];
end
9: begin
scroll_tile_base <= register_write_data;
end
10: begin
scroll_map_base <= register_write_data;
end
11: begin
layer_enable <= register_write_data[5:0];
end
12: begin
layer_enable_alpha_over <= register_write_data[7:0];
end
13: begin
scroll_use_wide_map <= register_write_data[3:0];
end
default: begin
`ifndef SYNTHESIS
$display("unimplemented register: %x", register_write_address);
$stop;
`endif
end
endcase
end else if (register_write_address[4]) begin
case (register_write_address[3:2])
0: scroll_x[register_write_address[1:0]] <= register_write_data;
1: scroll_y[register_write_address[1:0]] <= register_write_data;
endcase
end
end
end
// --- Register reads ---
always @(posedge clk) begin
case (register_read_address[1])
0: host_read_data <= raster_x;
1: host_read_data <= raster_y;
endcase
end
// --- Sprite metadata writing ---
reg [7:0] sprite_metadata_address;
reg [2:0] sprite_metadata_block_select;
wire [15:0] sprite_metadata_write_data = register_write_data;
reg sprite_metadata_write_en;
reg [2:0] sprite_metadata_block_select_nx;
wire sprite_meta_address_needs_increment = sprite_metadata_block_select[2];
always @* begin
sprite_metadata_block_select_nx = sprite_metadata_block_select;
if (sprite_metadata_write_en) begin
sprite_metadata_block_select_nx = {sprite_metadata_block_select[1:0], sprite_metadata_block_select[2]};
end
end
// --- Affine layer register mapping ---
// (affine disabled in vdp_lite)
// --- Palette RAM ---
reg [1:0] palette_write_address;
wire [11:0] pal_write_data = register_write_data;
reg palette_write_en;
wire [11:0] palette_output;
wire [7:0] palette_masked_read_address = prioritized_masked_pixel;
reg [15:0] palette_masked_output;
wire [1:0] palette_read_address = prioritized_pixel[1:0];
// Only 4 colors (1 transparent)
// Original is 256 colors (16 palettes of 16 each)
// Single palette only
ffram #(
.INIT_FILE(INIT_PALETTE_RAM),
.DATA_WIDTH(12),
.ADDR_WIDTH(2)
) palette_ram (
.clk(clk),
.wdata0(pal_write_data),
.addr0(palette_write_address),
.we0(palette_write_en),
.addr1(palette_read_address),
.rdata1(palette_output)
);
// --- Blender and RGB output ---
// (blender disabled in vdp_lite so original color is always used)
wire clipping_left = raster_x < (OFFSCREEN_X_TOTAL + CLIP_LEFT);
wire clipping_right = raster_x >= (OFFSCREEN_X_TOTAL + H_ACTIVE_WIDTH - CLIP_RIGHT);
wire clipping_rgb = clipping_left || clipping_right;
wire [11:0] output_color = (active_display && !clipping_rgb ? palette_output : 12'b0);
always @(posedge clk) begin
r <= output_color[11:8];
g <= output_color[7:4];
b <= output_color[3:0];
end
// --- Raster offset for scrolling layers ---
localparam SCROLL_START_LEAD_TIME = 32;
localparam SCROLL_OFFSCREEN_ADVANCE = -7;
reg [10:0] raster_x_offset;
localparam HEIGHT_DIFF = HEIGHT_TOTAL - 512;
wire scroll_base_x_start = raster_x == (OFFSCREEN_X_TOTAL - SCROLL_START_LEAD_TIME);
// the +1 is to preserve alignment between raster_x[2:0] (used to sequence VRAM access)
localparam SCROLL_BASE_X_INITIAL = SCROLL_OFFSCREEN_ADVANCE + 1;
always @(posedge clk) begin
raster_x_offset <= scroll_base_x_start ? SCROLL_BASE_X_INITIAL : raster_x_offset + 1;
end
// --- Raster offset for sprites ---
reg [9:0] sprites_x;
reg [8:0] sprites_y;
localparam SPRITE_X_INITIAL = -1;
localparam SPRITE_START_LEAD_TIME = 6;
localparam SPRITE_HOLD_TIME = HEIGHT_TOTAL - 512;
// alternatively instead of comparing all bits in raster_x, just check for all 1-bits
// since it counts up and resets to 0 predictably
wire sprites_x_start = (raster_x == (OFFSCREEN_X_TOTAL - SPRITE_START_LEAD_TIME + CLIP_LEFT));
wire sprites_x_reset = line_ended;
reg sprites_x_counting;
always @(posedge clk) begin
// used for line buffer read address for display
if (reset || sprites_x_reset) begin
sprites_x_counting <= 0;
end else if (sprites_x_start) begin
sprites_x_counting <= 1;
end
sprites_x <= (sprites_x_counting ? sprites_x + 1 : SPRITE_X_INITIAL);
end
localparam SPRITE_MAX_HEIGHT = 16;
localparam SPRITE_Y_DELAY = 2;
always @(posedge clk) begin
if (reset) begin
sprites_y <= 0;
end else if (line_ended) begin
if (raster_y == (HEIGHT_TOTAL - SPRITE_MAX_HEIGHT - SPRITE_Y_DELAY)) begin
sprites_y <= 512 - SPRITE_MAX_HEIGHT;
end else begin
sprites_y <= sprites_y + 1;
end
end
end
// --- Shared pixel row reverser ---
// Sprites and scroll layers can optionally have their graphics data flipped horizontally.
// Since only 1 of these can be read at a time, this reversing logic can be shared between all.
wire map_pixel_row_needs_x_flip = |(scroll_x_flip & scroll_char_load);
wire sprite_pixel_row_needs_x_flip = vram_sprite_read_data_needs_x_flip && vram_sprite_read_data_valid;
wire should_reverse_pixel_row = map_pixel_row_needs_x_flip || sprite_pixel_row_needs_x_flip;
reg [31:0] pixel_row_ordered;
always @* begin
if (should_reverse_pixel_row) begin
pixel_row_ordered[31:28] = vram_read_data_r[3:0];
pixel_row_ordered[27:24] = vram_read_data_r[7:4];
pixel_row_ordered[23:20] = vram_read_data_r[11:8];
pixel_row_ordered[19:16] = vram_read_data_r[15:12];
pixel_row_ordered[15:12] = vram_read_data_r[19:16];
pixel_row_ordered[11:8] = vram_read_data_r[23:20];
pixel_row_ordered[7:4] = vram_read_data_r[27:24];
pixel_row_ordered[3:0] = vram_read_data_r[31:28];
end else begin
pixel_row_ordered = vram_read_data_r;
end
end
// --- Scroll pixel generators ---
// (no scrolling layers in vdp_lite)
// --- Priority control ---
localparam [4:0] LAYER_ENABLE_MASK = (1 << (LAYERS_TOTAL)) - 1 | `LAYER_SPRITES_OHE;
wire [7:0] prioritized_pixel;
wire [4:0] prioritized_masked_layer;
wire [7:0] prioritized_masked_pixel;
// verilator lint_off UNUSED
wire [4:0] prioritized_layer;
// verilator lint_on UNUSED
wire [4:0] layer_mask = ~layer_enable_alpha_over;
vdp_priority_compute priority_compute(
.clk(clk),
.scroll0_pixel(8'b0),
.scroll0_is_8bpp(1'b0),
.scroll1_pixel(8'b0),
.scroll2_pixel(8'b0),
.scroll3_pixel(8'b0),
.sprite_pixel(sprite_pixel),
.sprite_priority(sprite_pixel_priority),
.layer_enable(layer_enable[4:0] & LAYER_ENABLE_MASK),
.layer_mask(layer_mask),
.prioritized_pixel(prioritized_pixel),
.prioritized_layer(prioritized_layer),
.prioritized_masked_layer(prioritized_masked_layer),
.prioritized_masked_pixel(prioritized_masked_pixel)
);
// --- VRAM bus arbiter ---
reg [15:0] vram_read_data_even_r, vram_read_data_odd_r;
wire [31:0] vram_read_data_r = {vram_read_data_odd_r, vram_read_data_even_r};
always @(posedge clk) begin
vram_read_data_even_r <= vram_read_data_even;
vram_read_data_odd_r <= vram_read_data_odd;
end
wire [3:0] scroll_meta_load;
wire [3:0] scroll_char_load;
wire load_all_scroll_row_data;
wire vram_written;
wire [3:0] scroll_palette [0:3];
wire [3:0] scroll_x_flip;
// Most of the inputs to this aren't used in this iteration as they require substantial RAM
// This could be revisited if a RAM compiler becomes available in a future run
vdp_vram_bus_arbiter_standard bus_arbiter(
.clk(clk),
.raster_x_offset(raster_x_offset),
.raster_x(raster_x),
.raster_y(raster_y),
// Scroll attributes
.scroll_x_0(10'b0), .scroll_x_1(10'b0), .scroll_x_2(10'b0), .scroll_x_3(10'b0),
.scroll_y_0(9'b0), .scroll_y_1(9'b0), .scroll_y_2(9'b0), .scroll_y_3(9'b0),
.scroll_tile_base(16'b0),
.scroll_map_base(16'b0),
.scroll_use_wide_map(4'b0),
// Affine attributes
.affine_enabled(1'b0),
.affine_offscreen(1'b0),
.affine_vram_address_even(), .affine_vram_address_odd(),
// Sprite attributes
.vram_sprite_address(vram_sprite_address),
// Output control for various functional blocks
.vram_written(vram_written),
.load_all_scroll_row_data(load_all_scroll_row_data),
.vram_sprite_read_data_valid(vram_sprite_read_data_valid),
.scroll_char_load(scroll_char_load),
.scroll_meta_load(scroll_meta_load),
// VRAM write control
.vram_port_write_en_mask(vram_port_write_en_mask),
.vram_write_address_16b(vram_write_address_16b),
.vram_write_data_16b(vram_write_data_16b),
// Output scroll attributes
.scroll_palette_0(scroll_palette[0]), .scroll_palette_1(scroll_palette[1]),
.scroll_palette_2(scroll_palette[2]), .scroll_palette_3(scroll_palette[3]),
.scroll_x_flip_0(scroll_x_flip[0]), .scroll_x_flip_1(scroll_x_flip[1]),
.scroll_x_flip_2(scroll_x_flip[2]), .scroll_x_flip_3(scroll_x_flip[3]),
// VRAM interface
.vram_read_data_even(vram_read_data_even_r),
.vram_read_data_odd(vram_read_data_odd_r),
.vram_address(vram_address),
.vram_write_data_even(vram_write_data_even), .vram_write_data_odd(vram_write_data_odd),
.vram_we_even(vram_we_even), .vram_we_odd(vram_we_odd)
);
// --- Sprites ---
wire [7:0] sprite_pixel;
wire [1:0] sprite_pixel_priority;
wire [13:0] vram_sprite_address;
wire vram_sprite_read_data_needs_x_flip;
wire vram_sprite_read_data_valid;
wire sprite_core_reset = line_ended;
wire line_buffer_hold = sprites_y[0];
vdp_sprite_core #(
// .INIT_X_BLOCK(INIT_X_BLOCK),
// .INIT_Y_BLOCK(INIT_Y_BLOCK),
// .INIT_G_BLOCK(INIT_G_BLOCK)
) sprites (
.clk(clk),
.start_new_line(sprite_core_reset),
// Drop LSB of countera to double pixel size
.render_x({1'b0, sprites_x[9:1]}),
.render_y({1'b0, sprites_y[8:1]}),
// .render_y(sprites_y[8:0]),
// Line buffer is normally cleared every line at full res
// Doubling height means the rows are repeated and must be held for an extra line
.line_buffer_hold(line_buffer_hold),
// .line_buffer_hold(1'b0),
.meta_address(sprite_metadata_address[4:0]),
.meta_write_data(sprite_metadata_write_data),
.meta_block_select(sprite_metadata_block_select),
.meta_we(sprite_metadata_write_en),
.vram_base_address(full_sprite_tile_base),
.vram_read_address(vram_sprite_address),
.vram_read_data(pixel_row_ordered),
.vram_read_data_needs_x_flip(vram_sprite_read_data_needs_x_flip),
.vram_data_valid(vram_sprite_read_data_valid),
.pixel(sprite_pixel),
.pixel_priority(sprite_pixel_priority)
);
// --- Affine layer ---
// (affine disabled in vdp_lite)
endmodule