The last blog covered the generation of background tiles. This time around we'll look at another facet of retro graphics. This one in my opinion is much more exciting:
Hardware Sprites!
Beginnings of a Journey
Growing up in the 1980s the spectacle of the arcades dazzled my young impressionable mind. The underlying sprite hardware technology that made all this magic possible remained a mystery. Even as I embarked on a career in electronics the secret sauce behind it all remained elusive. I had no idea how hardware sprites were implemented. Then one day in the fall of 2019 I was reading up on NEOGEO architecture. The rabbit hole eventually led me to the following site:
https://wiki.neogeodev.org/index.php?title=Line_buffers
It was here that the mystery was finally revealed....
Sprites are copied to line buffers!!!
This is illustrated in the diagram below.
The sprite's Y position is compared with the screen's current line number. This is done to see if the sprite will appear on the upcoming line. If the sprite is indeed to appear its relevant line will be copied over to the line buffer.
The criteria for deciding this is expressed simply enough in Verilog.
// See if sprite is in current line always @ (posedge clk) begin if ((line_number >= y_reg) & (line_number <(y_reg+16))) sprite_in_line <= 1'b1; else sprite_in_line <= 1'b0; end
The +16 term is because the sprites are 16 pixels high.
You may have noticed that there are two line buffers in the diagram above. While one buffer is being written to, the other is being read and displayed to the screen.
Selecting which line to copy is determined by the Sprite Pattern Table Address. The diagram below shows how this address is formed.
The most significant bits are derived from the Sprite Name attribute and are used to select the sprite's pattern.
The 4 least significant bits are used to select the specific line. They are obtained by subtracting the sprite Y position from the screen's line number.
Implementation
The FPGA implementation is depicted in the block diagram below.
The Attribute Table contains sprite parameters such as x-position, y-position and sprite name for all 64 sprites. Sprite name is used to specify which sprite pattern to select.
The Sprite Pattern Table as its name implies contains the patterns for the sprites. The data produced here is 32 bits. The sprites themselves are 16 pixels wide so this allows 2 bits of color selection for each pixel.
The Sprite Color Memory contains the 9-bit RGB for the sprite. The priority encoder at the far right side of the block diagram decides which RGB source ends up on top: Sprite, Score layer or Background Tile layer.
Last but not least, at the center of the diagram we find the Sprite Buffer Pair. This block contains the two line buffers as well as the logic required to direct data from the Attribute Table, Sprite Pattern Table and Color Memory. Its Verilog code, sprite_buffer_pair.v, and that of its individual line buffers, sprite_buffer.v, appear below.
sprite_buffer_pair.v
module sprite_buffer_pair //There are two line buffers for generating the sprites (sprite_buffer_a and sprite_buffer_b) // One line buffer will be in insertion mode while the other is in display mode // During insertion mode lines from a sprite's pattern are copied to the line buffer (If the sprite will be displayed on the next line) // When not in insertion mode the line buffer is read out and produces the RGB color #(parameter ATTRIBUTE_ADDR_WIDTH = 7, parameter COLOR_SEL_WIDTH = 2, //determines number of palettes i.e 2^ COLOR_SEL_WIDTH parameter PATTERN_TABLE_ADDR_WIDTH = 11, parameter PATTERN_TABLE_DATA_WIDTH = 32, parameter COLOR_WIDTH = 9, //width of RGB data parameter VIDEO_PIPELINE = 2 ) ( input clk, input reset_n, input synch, input buffer_sel, //Used to toggle which line buffer is in insertion mode input [15:0] attribute_data, //sprite x-position, y-position, pattern selection, etc. input [PATTERN_TABLE_DATA_WIDTH-1:0] pattern_table_data, input [COLOR_WIDTH-1:0] color, // Addresses for external memories output reg [3:0] color_address, output reg [ATTRIBUTE_ADDR_WIDTH-1:0] attribute_addr, output reg [PATTERN_TABLE_ADDR_WIDTH-1:0] sprite_pattern_address, output reg [COLOR_WIDTH:0] sprite_color //RGB color value output by selected sprite line buffer ); //buffer_sel = 1 sprite_buffer_a will be in insertion mode. So sprite_color_b will be selected for output //buffer_sel = 0 sprite_buffer_b will be in insertion mode. So sprite_color_a will be selected for output wire [3:0] color_address_a; wire [3:0] color_address_b; wire [ATTRIBUTE_ADDR_WIDTH-1:0] attribute_addr_a; wire [ATTRIBUTE_ADDR_WIDTH-1:0] attribute_addr_b; wire [PATTERN_TABLE_ADDR_WIDTH-1:0] sprite_pattern_address_a; wire [PATTERN_TABLE_ADDR_WIDTH-1:0] sprite_pattern_address_b; wire [COLOR_WIDTH:0] sprite_color_a; wire [COLOR_WIDTH:0] sprite_color_b; //multiplexor that decides the source for memory addresses as well as the source for the RGB color data to be displayed. always @ (posedge clk) begin case (buffer_sel) 1'b0: begin // addresses originate from line buffer b color_address <= color_address_b; attribute_addr <= attribute_addr_b; sprite_pattern_address <= sprite_pattern_address_b; sprite_color <= sprite_color_a; // we output RGB color from line buffer a end 1'b1: begin // addresses originate from line buffer a color_address <= color_address_a; attribute_addr <= attribute_addr_a; sprite_pattern_address <= sprite_pattern_address_a; sprite_color <= sprite_color_b; // we output RGB from line buffer b end endcase end //Instantiate sprite_buffer_a and sprite_buffer_b sprite_buffer #(.ATTRIBUTE_ADDR_WIDTH (ATTRIBUTE_ADDR_WIDTH), .COLOR_SEL_WIDTH (COLOR_SEL_WIDTH), .PATTERN_TABLE_ADDR_WIDTH (PATTERN_TABLE_ADDR_WIDTH), .PATTERN_TABLE_DATA_WIDTH (PATTERN_TABLE_DATA_WIDTH), .COLOR_WIDTH (COLOR_WIDTH),.VIDEO_PIPELINE (VIDEO_PIPELINE) ) sprite_buffer_a ( clk, reset_n, synch, buffer_sel, attribute_data, pattern_table_data, color, color_address_a, attribute_addr_a, sprite_pattern_address_a, sprite_color_a ); sprite_buffer #(.ATTRIBUTE_ADDR_WIDTH (ATTRIBUTE_ADDR_WIDTH), .COLOR_SEL_WIDTH (COLOR_SEL_WIDTH), .PATTERN_TABLE_ADDR_WIDTH (PATTERN_TABLE_ADDR_WIDTH), .PATTERN_TABLE_DATA_WIDTH (PATTERN_TABLE_DATA_WIDTH), .COLOR_WIDTH (COLOR_WIDTH),.VIDEO_PIPELINE (VIDEO_PIPELINE) ) sprite_buffer_b ( clk, reset_n, synch, (!buffer_sel), attribute_data, pattern_table_data, color, color_address_b, attribute_addr_b, sprite_pattern_address_b, sprite_color_b ); endmodule
sprite_buffer.v
module sprite_buffer // This block manages a line buffer for sprite generation // During insert_mode the logic in this block decides if a sprite needs to be displayed on the next line // If so the line is extracted from sprite pattern memory and this block will oversee the process involved in writing the sprite's RGB color to the line buffer // In doing so this block generates the addresses for the attribute memory, sprite pattern table and sprite color memory // When we aren't in insert_mode the line buffer is read out so that its RGB data can be displayed #(parameter ATTRIBUTE_ADDR_WIDTH = 7, parameter COLOR_SEL_WIDTH = 2, parameter PATTERN_TABLE_ADDR_WIDTH = 11, //(7 bits to select sprite + 4 bits per sprite) parameter PATTERN_TABLE_DATA_WIDTH = 32, parameter COLOR_WIDTH = 9, parameter VIDEO_PIPELINE = 2 ) ( input clk, // Designed to support 24MHz and 25MHz clocks input reset_n, input synch, // This signal synchronizes internal counters to the VGA timing input insert_mode, input [15:0] attribute_data, //x-pos, y-pos, pattern selction etc. input [PATTERN_TABLE_DATA_WIDTH-1:0] pattern_table_data, input [COLOR_WIDTH-1:0] color, output [3:0] color_address, output reg [ATTRIBUTE_ADDR_WIDTH-1:0] attribute_addr, output wire [PATTERN_TABLE_ADDR_WIDTH-1:0] sprite_pattern_address, output wire [COLOR_WIDTH:0] sprite_color ); // *************** Constants ********************** //Constants associated with horizontal_counter and verical_counter // horizontal_counter is used to keep track of our horizontal position // vertical counter tells us which line we are on // Eventually clock frequency should be brought in as a parameter. // For now the values for some of these parameters have to be manually changed according to comments below parameter HORIZONTAL_TERMINAL_COUNT = 767; //For 25MHz clock use 799 for 24MHz use 767 parameter VERTICAL_COUNTER_TERMINAL_COUNT = 524; //640 x 480 has 524 vertical lines if you include blanked lines parameter ENABLE_VIDEO_COUNT = 153; //Video stopes being blanked after this count. For 25MHz clock use 160; for 24MHz clock use 153 parameter ASSERT_INSERTION_ENABLE = 1; // Early in the horizontal line we begin writing parameter DEASSERT_INSERTION_ENABLE = HORIZONTAL_TERMINAL_COUNT - 2; parameter FIRST_LINE = 0; parameter LAST_LINE = 479; // line 479 is the last visible line parameter ASSERT_EXTRACT = ENABLE_VIDEO_COUNT - VIDEO_PIPELINE; // We only start displaying during the visible portion of the horizontal line // We are subracting VIDEO_PIPELINE because we have to start earlier to account for pipeline dealy parameter DEASSERT_EXTRACT = HORIZONTAL_TERMINAL_COUNT - VIDEO_PIPELINE; // Constants used to decode control_counter count // Decodes of the control_counter count are used to extract parameters from the attribute memory like y-position and x-position // Additionally other decodes decide when to assess the sprite to see if it appears in the next line and when to load it in the shift register parameter CONTROL_COUNTER_TC = 30; //number where control_counter rolls over parameter LOAD_Y_REG = 3; // When control_counter = LOAD_Y_REG we save a copy of the sprite's y-position parameter NEXT_X_REG = LOAD_Y_REG; // When control_counter = NEXT_X_REG we increment the attribute memory address to access the sprite's x-position parameter LOAD_X_REG = NEXT_X_REG+3; // At this count we save a copy of the sprite's x-position parameter NEXT_Y_REG = LOAD_X_REG; // At this count we increment the attribute memory so that later on we can read the y-position parameter ASSESS_SPRITE_SKIP = NEXT_Y_REG; // At this count we use the stored y-position to see if the sprite appears in the next line parameter LOAD_WRITE_ADDRESS = NEXT_Y_REG + 1; // At this count we load the line buffer's write_address so that the first pixel can be written parameter LOAD_PATTERN_REGISTER = LOAD_WRITE_ADDRESS+2; // An entire line from the sprite_pattern_table is loaded into the shift register parameter ENABLE_SHIFT_COUNT = LOAD_PATTERN_REGISTER; // We start shifting pixels out of the shift register parameter DISABLE_SHIFT_COUNT = ENABLE_SHIFT_COUNT + 15; // We stop shifting pixels out of the shift register parameter ASSERT_BUFFER_WRITE = ENABLE_SHIFT_COUNT + 2; // We start writing on this count parameter DEASSERT_BUFFER_WRITE = ASSERT_BUFFER_WRITE + 16; // We stop writing on this account parameter INCREMENT_SPRITE_COUNT = CONTROL_COUNTER_TC; //We increment the sprite_counter because we don't want to exceed maximum number of sprites per line // The attribute table is used for storing parameters such as x-position, y-position, x-flip, y-flip, sprite palette and sprite name (i.e. which sprite to select) // Tentatively the attribute table is 128 addresses deep supporting 64 sprites // ************ ATTRIBUTE TABLE FORMAT **************** // Even Addresses // [15..9] = sprite_name // [8..0] = x-position // Odd Addresses // [13..12] = color_palette_sel[1..0] // [11] = sprite_enable // [10] = y-flip // [9] = x-flip // [8] = y-position // ***************************************************** //Constants associated with attribute table parameter X_REG_WIDTH = 9; parameter Y_REG_WIDTH = 9; parameter SPRITE_ENABLE_MASK = 11; parameter X_FLIP_MASK = 9; parameter Y_FLIP_MASK = 10; parameter PALETTE_SEL_MASK = 12; // MISC Constants parameter NUMBER_OF_SPRITES = 64; // Represents the maximum number of sprites per line parameter FIRST_ATTRIBUTE_ADDRESS = 127; // We count backwards from 127 down to 0. This means that sprite 0 will be highest priority parameter COLOR_SEL_DELAYED = 2; //This value is used to align data upper and lower portions of color memory address //Signals reg [9:0] horizontal_counter; // Tells us where we are in the line reg [9:0] vertical_counter; // Tells us which vertical line we are on reg insertion_enable; reg [4:0]control_counter; // Decodes of this counter are used to see if sprite is in the next line and to copy sprite to line buffer reg [6:0] sprite_counter; // Counts number of sprites on a line reg [8:0] write_address; // Write address for line buffer reg [9:0] read_address; // Read address for line buffer reg buffer_write; // When '1' we write to the line buffer reg [8:0] line_number; // The current line we are on reg [3:0] sprite_pattern_line; // Which sprite line do we dislay in the next line reg sprite_in_line; // Signals that the sprite is in the next line and needs to be written reg [COLOR_WIDTH-1:0] delayed_color_sel [COLOR_SEL_DELAYED-1:0]; // Forms most significant portion of the color memory adddress // It is delayed to allign it with the color bits being shifted out of the shift register reg [PATTERN_TABLE_DATA_WIDTH-1:0]pattern_register; //This is a shift register reg shift_pattern; // When '1' we shift pixels out of the shift_register 2 bits at time. Forms 2 least significant bits of color memory address reg extract_from_buffer; //regsiters that are latched from attribute memory reg [X_REG_WIDTH-1:0] x_reg; // We use this register to store a copy of the x-position reg [Y_REG_WIDTH-1:0] y_reg; // We use this register to store a copy of the y-position reg sprite_enable; // Is sprite enabled or turned off reg x_flip; // 0 not flipped, 1 horizontal flip reg y_flip; // 0 not flipped, 1 vertical flip reg [15:X_REG_WIDTH]sprite_name; //Selects pattern for sprite. Forms most significant portion of sprite name table address reg [1:0] palette_sel; // selects which palette to use for sprite color wire its_a_pixel; // 0 = transparent, 1 = actual pixel wire [COLOR_WIDTH:0] write_data; // RGB value from color_memory wire [COLOR_SEL_WIDTH-1:0] color_sel; // obtained from shift register. Forms 2 least significant bits of color_memory address //******************************************************** // horizontal_counter and vertical_counter are local counters that // track the horizontal and vertical positions of the lines used in VGA // the synch signal is used to synchronize these counters to the video timing //******************************************************** //horizontal_counter always @ (posedge clk or negedge reset_n) begin if (!reset_n) horizontal_counter <= 0; else begin if (synch) horizontal_counter <= 0; else if (horizontal_counter == HORIZONTAL_TERMINAL_COUNT) horizontal_counter <= 0; else horizontal_counter <= horizontal_counter +1; end end //vertical_counter always @ (posedge clk or negedge reset_n) begin if (!reset_n) vertical_counter <=0; else begin if (synch) vertical_counter <=0; else if (horizontal_counter == HORIZONTAL_TERMINAL_COUNT) begin if (vertical_counter == VERTICAL_COUNTER_TERMINAL_COUNT) vertical_counter <= 0; else vertical_counter <= vertical_counter + 1; end end end // insertion_enable // insertion_enable is active when we are in the general process of writing to the line buffer // We only assert when lsb of vertical_counter is '0' because we stay in insertion mode for 2 consequtive lines (240 lines instead of VGA's 480 lines) // If we exceed the maximum number of sprites per line insertion_enable is deactivated always @ (posedge clk or negedge reset_n) begin if (!reset_n) insertion_enable <=1'b0; else begin if (synch) insertion_enable <=1'b0; else if ((insert_mode) & (vertical_counter < (LAST_LINE + 1)) ) begin if ((horizontal_counter == ASSERT_INSERTION_ENABLE) & (vertical_counter[0] == 1'b0)) insertion_enable <= 1'b1; else if (sprite_counter >(NUMBER_OF_SPRITES-1)) insertion_enable <= 1'b0; end end end // control_counter // control_counter counts are decoded to extract data from attribute memory, determine if sprite is in next line and to copy sprite to the line buffer // if it is determined that the sprite is not in the line it will be reset // otherwise we count to CONTROL_COUNTER_TC always @ (posedge clk or negedge reset_n) begin if (!reset_n) control_counter <= 0; else begin if(synch | !insertion_enable) control_counter <= 0; else begin if (sprite_counter >= NUMBER_OF_SPRITES) //*** reset if we exceed the maximum number of sprites per line control_counter <=0; else if (control_counter == CONTROL_COUNTER_TC) control_counter <=0; else if ((control_counter == ASSESS_SPRITE_SKIP) & ((sprite_enable == 1'b0) | (sprite_in_line == 1'b0))) control_counter <= CONTROL_COUNTER_TC; //skip to end count else control_counter <= control_counter + 1; end end end //********************************************** // sprite counter goes from 0 to 63 (technically 64) //********************************************** always @ (posedge clk or negedge reset_n) begin if (!reset_n) sprite_counter <= 0; else begin if (!insertion_enable) sprite_counter <= 0; else if (control_counter == INCREMENT_SPRITE_COUNT) begin sprite_counter <= sprite_counter + 1; end end end //process for attribute_addr //This counter starts at 127 // It is decremented at control_counter decodes NEXT_X_REG and NEXT_Y_REG // Having the control_counter count down in this manner allows SPRITE 0 to have the higest priority // Highest priority in this context means that a pixel from SPRITE 0 will be displayed over top pixels from the other sprites always @ (posedge clk) begin if ((insert_mode) & (horizontal_counter == ASSERT_INSERTION_ENABLE) & (vertical_counter[0]==1'b0)) attribute_addr <= FIRST_ATTRIBUTE_ADDRESS; // tentatively this is 127 else if ((control_counter == NEXT_Y_REG) | (control_counter == NEXT_X_REG)) begin if (attribute_addr == 0) attribute_addr <= FIRST_ATTRIBUTE_ADDRESS; else attribute_addr = attribute_addr -1; //decrement address end end //Latching y_reg and other parameters from odd number addresses of attribute mempry always @ (posedge clk) begin if (control_counter == LOAD_Y_REG) begin y_reg <= attribute_data[Y_REG_WIDTH-1:0]; x_flip <= attribute_data[X_FLIP_MASK]; y_flip <= attribute_data [Y_FLIP_MASK]; palette_sel <= attribute_data[PALETTE_SEL_MASK+1:PALETTE_SEL_MASK]; sprite_enable <= attribute_data[SPRITE_ENABLE_MASK]; end end //Latching x_reg and sprite_name (from even number addresses of attribute memory) always @ (posedge clk) begin if (control_counter == LOAD_X_REG) begin x_reg <= attribute_data[X_REG_WIDTH-1:0]; sprite_name <= attribute_data[15: X_REG_WIDTH]; end end // See if sprite is in current line always @ (posedge clk) begin if ((line_number >= y_reg) & (line_number <(y_reg+16))) sprite_in_line <= 1'b1; else sprite_in_line <= 1'b0; end //Figuring out the 4 LSBs of sprite_pattern_address always @ (posedge clk) begin if (y_flip) sprite_pattern_line <= (15 - (line_number - y_reg )); else sprite_pattern_line <= (line_number - y_reg); end //sprite_pattern_address is comprised from sprite_name and sprite_pattern_line assign sprite_pattern_address = {sprite_name, sprite_pattern_line}; // shift_pattern .... while this signal is active the shift register will shift always @ (posedge clk or negedge reset_n) begin if (!reset_n) shift_pattern <= 1'b0; else begin if (synch) shift_pattern <= 1'b0; else if (control_counter == ENABLE_SHIFT_COUNT) shift_pattern <= 1'b1; else if (control_counter == DISABLE_SHIFT_COUNT) shift_pattern <= 1'b0; end end //pattern_register // pattern register is loaded from the sprite_pattern_memory // The width of pattern_register is 32 bits // Data is shifted 2 bits to the left // These two bits form the 2 least significant bits of the color_memory address // If x_flip is set value from sprite_pattern_memory has to loaded in reverse always @ (posedge clk or negedge reset_n) begin if (!reset_n) pattern_register <= 0; else begin if (control_counter == LOAD_PATTERN_REGISTER) begin if (x_flip) begin pattern_register[31:24] <= {pattern_table_data[1:0], pattern_table_data[3:2], pattern_table_data[5:4], pattern_table_data[7:6]}; pattern_register[23:16] <= {pattern_table_data[9:8], pattern_table_data[11:10], pattern_table_data[13:12], pattern_table_data[15:14]}; pattern_register[15:8] <= {pattern_table_data[17:16], pattern_table_data[19:18], pattern_table_data[21:20], pattern_table_data[23:22]}; pattern_register[7:0] <= {pattern_table_data[25:24], pattern_table_data[27:26], pattern_table_data[29:28], pattern_table_data[31:30]}; end else pattern_register[31:0] <= pattern_table_data[31:0]; end else if (shift_pattern) begin pattern_register[PATTERN_TABLE_DATA_WIDTH-1:2] <= pattern_register[PATTERN_TABLE_DATA_WIDTH-3:0]; //shift 2 bits to the right pattern_register[1:0] <= 2'b00; //fill most significant bits with 0 end end end // Mapping color_address assign color_sel = pattern_register[PATTERN_TABLE_DATA_WIDTH-1:PATTERN_TABLE_DATA_WIDTH-2]; assign color_address = {palette_sel, color_sel}; //*********************************************************** // write_address //*********************************************************** // initial write_address is determined by sprite's x-position // write_address will then be incremented to ensure all 16 pixels are written to line buffer always @ (posedge clk or negedge reset_n) begin if (!reset_n) write_address <= 0; else begin if (!insertion_enable) write_address <= 0; //hold at reset if we are not in the process of 'inserting' else if (control_counter == LOAD_WRITE_ADDRESS) write_address <= x_reg; else if (buffer_write) write_address <= write_address +1; end end //*********************************************************** // buffer_write //*********************************************************** always @ (posedge clk or negedge reset_n) begin if (!reset_n) buffer_write <= 1'b0; else begin if (control_counter == ASSERT_BUFFER_WRITE) buffer_write <= 1'b1; else if (control_counter == DEASSERT_BUFFER_WRITE) buffer_write <= 1'b0; end end // Line number // We disregard lsb to effectively give us 240 lines instead of 480 lines always @ (posedge clk) begin line_number <= vertical_counter[9:1]; end // extract_from_buffer is essentially a counter enable for the line buffer's read address. always @ (posedge clk or negedge reset_n) begin if (!reset_n) extract_from_buffer <= 1'b0; else if((vertical_counter < (LAST_LINE+1)) & (!insert_mode)) begin if (horizontal_counter == ASSERT_EXTRACT) extract_from_buffer <= 1'b1; else if (horizontal_counter == DEASSERT_EXTRACT ) extract_from_buffer <= 1'b0; end end //Delaying color_sel to align with RGB data extracted from the color_memory // When color_sel is 0 we consider the pixel transparent // delayed_color_sel is used to determine if its a visible pixel or transparent integer p; always @ (posedge clk) begin delayed_color_sel[0] <= color_sel; for (p = 1; p < COLOR_SEL_DELAYED; p = p +1)begin delayed_color_sel[p] <= delayed_color_sel[p-1]; end end // if delayed_color_sel is not 0 then we have a pixel assign its_a_pixel = (delayed_color_sel[COLOR_SEL_DELAYED-1] == 2'b00) ? 1'b0 : 1'b1; // Logic 0 means transparent, Logic 1 means valid pixel assign write_data = {its_a_pixel,color}; // We tack on its_a_pixel to write_data //Special provisions to clear sprite memory after second line is displayed wire [8:0] selected_write_address; wire [31:0] selected_write_data; reg clear_write; reg [9:0]clear_address; // As we display RGB from the line buffer we also clear its contents // This gives us a clean slate prior to the line_buffer entering insert mode always @ (posedge clk) begin clear_address <= read_address; clear_write <= (extract_from_buffer & (vertical_counter[0])); end // select source for line_buffer's write_address assign selected_write_address = (clear_write)? (clear_address[9:1]) : write_address; // select source for line buffer's write_data assign selected_write_data = (clear_write)? (10'b0000000000) : write_data; // instantiation of line_buffer tile_memory #(.ADDRESS_WIDTH (9),.DATA_WIDTH (COLOR_WIDTH+1)) sprite_mem(clk, read_address[9:1], selected_write_address, selected_write_data, ((buffer_write &(write_data[9])) | clear_write), sprite_color); //read_address always @ (posedge clk) begin if (extract_from_buffer) read_address <= read_address +1; else read_address <= 0; end endmodule
Interface with 6502
At this stage I am still controlling the graphics blocks using the 6502 processor core. Sprite attributes such as x-position and y-position are controlled through 128 addresses in the 6502 address space. Two attribute table addresses are devoted to each of the 64 sprites. The format is as follows:
One of the charms of the 6502 is its 8-bit bus width. However, a number of the graphics functions require 16 bits and in the case of the Sprite Pattern Table a full 32 bits are required. To get around this 4 holding registers were added to the design.
First Design Example
In a lot of ways I am doing this project to satisfy old childhood dreams, so when it came to developing a demo I had to reach back through the years and come up with something to please a certain child of the 1980s.
This is what I came up with.
Skulls held a certain cachet back in the 80s so I imagine my younger self would be pleased.
For those wondering I used the Pixel Studio App to design the sprite. I feel obligated to mention that this free cell phone App is supported with pop up adds. On a positive note I was able to design the sprites while enjoying free coffee at the restaurant in my local Ikea.
For the demo itself I had the skull sliding across the background we designed in the previous blog.
To see this in motion click the video below.
The perceptive among us will spot a problem as they watch. As the skull moves across the screen there is a certain wobble in its display almost as if its pixels are being modulated with a waveform of some kind. To make things a little more perceptible at the end of the video the skull is replaced with a series of vertical lines.
While I don't know for certain I think the root of the problem is in how modern LCD screens handle VGA signals. It appears that a PLL is used within the display to derive the pixel clock. The ideal pixel clock for 640 x 480 VGA is 25.175MHz. My pixel clock, which is derived from the OrangeCrab's onboard oscillator, is 24MHz. I ported the design to the QMTECH Cyclone IV board where I had the ability to generate a 25MHz pixel clock. In this case there was no perceptible wobble. I've ordered a 25MHz crystal oscillator and in the future I'll probably install it on the OrangeCrab. Since there is a risk of damaging the board I decided to delay this rework until the digital design is complete.
Sprites per Scan Line
For those of you that remember playing the NES you will recall that once the screen gets crowded the sprites start to flicker. You've learned from this blog that sprite pixels are transferred from the Pattern Table to line buffers. The length of a scan line is 63.6us and 64us for NTSC and PAL respectively. The clock frequency of the NES pixel clock is 5.35MHz. Needless to say only a finite number of pixels from background and sprite layers can be transferred to line buffers in any given scan line. In fact a console's maximum number of sprites per scan line is a well reported spec. For the NES it is 8 sprites per scan line. For the Coleco Vision which makes use of the TMS9928 Video Display Controller, the maximum number of sprites per line is 4.
Sprites per Scan Line Demo
One of my goals when designing the sprite functionality was to not be hindered by a restrictive sprites per line limit. At the very least I wanted to ensure that 16 sprites could appear on a scan line without issue.
As I put the demo together I decided to avoid the regular video game cliches and instead tip my hat to old Hollywood. Doesn't the artwork below scream MGM musical?
Failing that, perhaps it pays tribute to Showgirls which is an underappreciated classic in its own right.
The following video shows our demonstration in action. Our goal of 16 sprites per line was exceeded. In fact we have 24 little showgirls sharing the stage. Can you imagine the production costs?
Final Demo
Even with my limited abilities as a pixel artist I managed to have a lot of fun putting these sprite demos together. It reminds me that, even with its limitations, the old hardware of yesterday can be used as a creative outlet. In more capable hands than my own, 8-bit hardware can be used to create experiences and visuals that still capture the imagination even today.
I don't aspire to create my own masterpiece but I certainly appreciate what this kind of tech offers as a creative medium. With that said, please bear with me as I present the final demo.
Here's the first bit of pixel art I developed for the demo.
Is it Clark Gable, or is it the Blackjack dealer from that Intellivision pack-in game? There is always a level of ambiguity with low res graphics but that cluster of pixels is supposed to be Charles Bronson.
The demo itself combines a number of sprites to make up a larger character. The sprites are then set to move in unison. The true sadists among us are free to read through the 6502 assembly code below.
; **************** MECHA.ASM *********************** ; This is a demo program that moves a mechanized Charles Bronson character around ; It also features a scrolling star-field background ;VARIABLES TICK_COUNTER EQU $0; LED_COPY EQU $1; LED EQU $80; Mapped to LED hardware SECOND_COUNTER EQU $85; ; ************* SCREEN SCROLL REGISTERS ************* X_SCROLL_REG EQU $1001; Y_SCROLL_REG EQU $1002; ; ********************************************************** ; ************ ADDRESSES FOR VIDEO GENERATION ************** ; *********************************************************** BG_COLOR EQU $1000 ;This is the screen's background color ; ***** HARDWARE ADDRESSES FOR GRAPHICS FUNCTION ***** COLOR_MEMORY EQU $1020; SPRITE_COLOR_MEM EQU $1040; SPRITE_ATTR EQU $1080; SPRITE_PATTERN EQU $4000; PATTERN_TABLE EQU $2000; NAME_TABLE EQU $6000; ;******** HOLDING REGISTERS ************* ; The graphics hardware is controlled with 16-bit and 32-bit data ; The 8-bit data is latched one byte at a time into the 4 holding registers below. HOLDING_REGISTER_A EQU $81 ; For data bits 7..0 HOLDING_REGISTER_B EQU $82 ; For data bits 15..8 HOLDING_REGISTER_C EQU $83 ; For data bits 23..16 HOLDING_REGISTER_D EQU $84 ; For 32 data bits 31..24 ;******* SPRITE COPY ********** ; This is our working copy of sprite attribute memory ; At the beginning of the 60Hz interrupt we copy this over to SPRITE_ATTR X_LO EQU $200; X_HI EQU $240; Y_LO EQU $280; Y_HI EQU $2C0; ; ************* VARIABLES FOR CHARLES BRONSON CHARACTER'S MOVEMENT **************** ; The Charles Bronson character is composed of a number of different sprites ; We want to move each of these sprites in unision ; Ultimately we want to to move him in a number of different dircetions, angles and speeds ; Each linear moevment will be called a TRIP ; A TRIP will be defined by X_STEP, Y_STEP and TRIP_TIME ; The Charles Bronson character's x and y position will be handled by X_OFFSET and Y_OFFSET variables ; X_OFFSET and Y_OFFSET will be added to each of the sprites origianl x and y positions. ; In this manner they will move in unision. TRIP_COUNTER EQU $86; This variable tells us what trip we are on TRIP_TIME EQU $87; This variable is a count down timer that is decremented on each 60Hz interrupt ; X_STEP varaibles....... X_STEP_FRACTIONAL EQU $88 ; fractional portion of X_STEP. Its resolution is 1/256 of a pixel X_STEP EQU $89 ; Main portion of X_STEP for horizontal pixels 0 through 255 X_STEP_MSb EQU $8A ; Generally serves as sign bit for X_STEP ; Y_STEP variables........ Y_STEP_FRACTIONAL EQU $8B; fractional position of Y_STEP. Its resolution is 1/256 of a pixel Y_STEP EQU $8C ; Main portion of Y_STEP for vertical pixels 0 through 255 Y_STEP_MSb EQU $8D ; Generally serves as sign bit for Y_STEP ; X_OFFSET variables ; These variables are used for the character's horizontal position ; They are divided in 3 parts just like X_STEP X_OFFSET_FRACTIONAL EQU $8E; X_OFFSET EQU $8F; X_OFFSET_MSb EQU $90; ; Y_OFFSET variables ; These variables are used for the character's vertical position ; They are divided in 3 parts just like Y_STEP Y_OFFSET_FRACTIONAL EQU $91; Y_OFFSET EQU $92; Y_OFFSET_MSb EQU $93; ; ***************** VARIABLES FOR CITY SKY LINE ****************** BUILDING_START EQU $94; ; ************** VARIABLES FOR SCROLL ***************** X_SCROLL_LSB EQU $95; X_SCROLL EQU $96; X_SCROLL_MSb EQU $97 Y_SCROLL_LSB EQU $98; Y_SCROLL EQU $99; ; **************** ZERO PAGE POINTERS ***************** SOURCE_POINTER EQU $10; SOURCE_POINTER_MSB EQU $11; DEST_POINTER EQU $12 DEST_POINTER_MSB EQU $13; ;CONSTANTS ONE_SECOND EQU $3C; NUMBER_OF_STARS EQU 14; ; Constants for background tile names BLANK_TILE EQU $0 STRIPE_TILE EQU $1 BUILDING_TILE EQU $2 STAR_TILE EQU $3 ; ************ ATTRIBUTE TABLE FORMAT **************** ; // Even Addresses ; // [15..9] = sprite_name ; // [8..0] = x-position ; // Odd Addresses ; // [13..12] = color_palette_sel[1..0] ; // [11] = sprite_enable ; // [10] = y-flip ; // [9] = x-flip ; // [8..0] = y-position ; ***************************************************** ; Constants related to sprites: SPRITE_XPOS_MSB_MASK EQU $1; SPRITE_X_MSB_FLIPS_MASK EQU $7; SPRITE_X_FLIP_MASK EQU $2; SPRITE_ENA_MASK EQU $8; XFLIP_CLR EQU $FD ; used for clearing X-FLIP bit NAME_CLR EQU $1 ; used for clearing Sprite Name LAST_TRIP EQU 7; When the TRIP_COUNTER reaches this number it is reset INCLUDE SPRITE_CONSTANTS.ASM; ;***** MAIN PROGRAM ****** .org $E000 RESET: SEI ; disable IRQs. Note NMI interrupts will continue to be enabled CLD ; disable decimal mode LDX #$FF ; TXS ; This sets stack pointer to $1FF ($100 + $FF) ; *** Initialize LED ****** LDA #$1; STA LED_COPY; STA LED; LDA #$0; STA TICK_COUNTER; ; ****** Set screen's backround color LDA #$82; STA HOLDING_REGISTER_A; LDA #$0; STA HOLDING_REGISTER_B; STA BG_COLOR; 082 corresponds to some sort of purple color ;*** Initialize COLOR MEMORY ; The COLOR MEMORY data width is 9-bits. ; So we utilize holding registers A & B to store data. LDX #$0; COLOR_LOOP: LDA COLORS_LO, X; STA HOLDING_REGISTER_A; LDA COLORS_HI, X; STA HOLDING_REGISTER_B; STA COLOR_MEMORY, X; INX; CPX #$10; BNE COLOR_LOOP; ;**** Programming Pattern Table ; Data width of Pattern Table is 16-bits ; Lower 8-bits are transfered from PATTERN_LO to HOLDING_REGISTER_A ; Upper 8-bits are transfered from PATTERN_HI to HOLDING_REGISTER_B ; We are going to only use 5 tiles. ; 0 BLANK_TILE ; 1 STRIPE_TILE ; 2 BUILDING_TILE ; 3 STAR_TILE LDX #$0; PATTERN_LOOP: LDA PATTERN_LO, X; STA HOLDING_REGISTER_A; LDA PATTERN_HI, X; STA HOLDING_REGISTER_B; STA PATTERN_TABLE, X; INX; CPX 32; 32 times through the loop is enough for first 4 patterns BNE PATTERN_LOOP; ; **** Program Name Table ; We will program the name table in nested loops using both X and Y Indexes ; Y will be used for columns and has a range of 0 to 37 ; X is for rows and has a range of 0 to 29 ; The idea here is to fill the entire screen with blank tiles ; DEST_POINTER will be initialized to $6000 which is the start of the NAME_TABLE ; Initialize holding register with BLANK_TILE LDA #BLANK_TILE; STA HOLDING_REGISTER_A; LDA #$0; STA HOLDING_REGISTER_B; ; Initialize Desintiatnion Pointer LDA #$0; STA DEST_POINTER; LDA #(NAME_TABLE/$100); STA DEST_POINTER_MSB; LDA #$0; TAX; TAY; BLANK_OUTER: LDY #$0; BLANK_INNER: STA (DEST_POINTER), Y; INY; CPY #38; BNE BLANK_INNER; ; Increment pointer by 38 CLC; LDA DEST_POINTER; ADC #38; STA DEST_POINTER; LDA DEST_POINTER_MSB; ADC #$0; i.e. add carry STA DEST_POINTER_MSB; INX; CPX #30; BNE BLANK_OUTER; ; ********* Adding Some Stars *********** LDA #STAR_TILE; STA HOLDING_REGISTER_A ;We will only be writing STAR_TILE in next loop LDA #$0; STA HOLDING_REGISTER_B; TAX ; Initialize X to 0 TAY; STAR_LOOP: LDA STAR_LSB_LIST, X; STA DEST_POINTER; LDA STAR_MSB_LIST, X STA DEST_POINTER_MSB; STA (DEST_POINTER), Y; INX; CPX NUMBER_OF_STARS; BNE STAR_LOOP; ; ******** DRAWING SKY LINE ********** ; After initializing the screen with blank tiles and adding stars we draw the sky line ; We intialize Destination Pointer to NAME_TABLE + 24 x 38 (0x6390) ; Outer loop traverses accross the screen ; Inner loop goes row by row painting the buildings LDA #$0; STA HOLDING_REGISTER_B; upper 8 bits will remain 0 LDY #$0; SKY_OUTER_LOOP: LDA BUILDING_START_LIST, Y; See which row building starts STA BUILDING_START; LDX #$0; ; We have to intialize the inner pointer prior to entering inner loop LDA #$44; STA DEST_POINTER; LDA #$63; STA DEST_POINTER_MSB; SKY_INNER_LOOP: TXA; CLC; ADC #22; CMP BUILDING_START; BMI ADJUST_SKY_POINTER; ; Otherwise if (X + 22) >/= BUILDING_START.... ; We write a building block LDA #BUILDING_TILE; STA HOLDING_REGISTER_A; STA (DEST_POINTER) , Y; ADJUST_SKY_POINTER: ; increment pointer for next row CLC; LDA DEST_POINTER; ADC #38; STA DEST_POINTER; LDA DEST_POINTER_MSB; ADC #$0; i.e. add carry bit STA DEST_POINTER_MSB; INX; CPX #8; Buildings exist between lines 22 and 29 BNE SKY_INNER_LOOP; INY; CPY #38; BNE SKY_OUTER_LOOP; ; CLEAR ALL 64 SPRITE ATTRIBUTES LDX #$0; CLR_ATTR_LOOP: STA X_LO, X; STA X_HI, X; STA Y_LO, X; STA Y_HI, X; INX; CPX #128; BNE CLR_ATTR_LOOP; ; PROGRAM SPRITE ATTRIBUTES ; We will program attributes fro sprites 3 through 23 ; Programming xposition and sprite name ; Programming yposition and color palette LDX #3; ATTR_INIT_LOOP: LDA X_INIT_LO, X; STA X_LO, X; LDA X_INIT_HI, X; STA X_HI, X; ;INY; LDA Y_INIT_LO, X; STA Y_LO, X; LDA Y_INIT_HI, X; STA Y_HI, X; INX; CPX #24; BNE ATTR_INIT_LOOP; ; ****** Initialize Sprite Color Table LDX #$0; INIT_SPR_CLR: LDA SPRITE_COLORS_LO, X; STA HOLDING_REGISTER_A; LDA SPRITE_COLORS_HI, X; STA HOLDING_REGISTER_B; STA SPRITE_COLOR_MEM, X; INX; CPX #$10; BNE INIT_SPR_CLR; ; ***** Program Sprite patterns ; We only use 16 sprite patterns LDX #$0; PRG_SPR_PAT: LDA SPR_PAT_A, X; STA HOLDING_REGISTER_A; LDA SPR_PAT_B, X; STA HOLDING_REGISTER_B; LDA SPR_PAT_C, X; STA HOLDING_REGISTER_C; LDA SPR_PAT_D, X; STA HOLDING_REGISTER_D; STA SPRITE_PATTERN, X; INX; CPX #$E0; We have 14 sprites 14 x 16 = E0 in hex BNE PRG_SPR_PAT; ; **** Reset SECOND_COUNTER; LDA #$0; STA SECOND_COUNTER; ; *** Reset Counter for managing movement LDA #$0; STA TRIP_TIME; LDA #LAST_TRIP; STA TRIP_COUNTER; ; Initialize X Scroll Variables LDA #$0; STA X_SCROLL_LSB; STA X_SCROLL; STA X_SCROLL_MSb; Forever: ;jump back to Forever, infinite loop JMP Forever; ; ********* Subroutines ****************** INITIALIZE_TRIP: LDX TRIP_COUNTER; LDA X_STEP_FRAC_LIST, X; STA X_STEP_FRACTIONAL; LDA X_STEP_LIST, X; STA X_STEP; LDA X_STEP_MSb_LIST, X; STA X_STEP_MSb; LDA Y_STEP_FRAC_LIST, X; STA Y_STEP_FRACTIONAL; LDA Y_STEP_LIST, X; STA Y_STEP; LDA Y_STEP_MSb_LIST, X; STA Y_STEP_MSb; LDA TRIP_TIME_LIST, X; STA TRIP_TIME; RTS; NMI: ;Push Registers to the stack PHP; push status reg PHA; push accumulator TXA; PHA; push x TYA; PHA; push y ; ***** Transfer our copy of sprite attributes to the actual sprite attribute memory LDX #$0; LDY #$0; COPY_SPRITES: LDA X_LO, X; STA HOLDING_REGISTER_A; LDA X_HI, X; STA HOLDING_REGISTER_B; STA SPRITE_ATTR, Y; INY; LDA Y_LO, X; STA HOLDING_REGISTER_A; LDA Y_HI, X; STA HOLDING_REGISTER_B; STA SPRITE_ATTR, Y; INY; INX; CPX #$40; BNE COPY_SPRITES; ; ********* UPDATING SCROLL ******** CLC; LDA X_SCROLL_LSB; ADC #$40; STA X_SCROLL_LSB; LDA X_SCROLL; ADC #$0 ; i.e. add carry bit STA X_SCROLL; BCS SET_X_SCROLL_MSb; CMP #$30; Detect end of screen 38 x 8 = 304 or 0x130 (Here we compare LSB) BNE UPDATE_X_SCROLL_REG; LDA X_SCROLL_MSb; CMP #$1; Remember we are making a comparision to 0x130. This time the MSB BNE UPDATE_X_SCROLL_REG; LDA #$0; At end of screen roll scroll counter over to 0 STA X_SCROLL; STA X_SCROLL_MSb; JMP UPDATE_X_SCROLL_REG; SET_X_SCROLL_MSb: LDA #1; STA X_SCROLL_MSb; UPDATE_X_SCROLL_REG: LDA X_SCROLL; STA HOLDING_REGISTER_A; LDA X_SCROLL_MSb; STA HOLDING_REGISTER_B; STA X_SCROLL_REG; ; **** Updating heart beat counter ****** LDX TICK_COUNTER; INX; STX TICK_COUNTER; CPX #ONE_SECOND; BNE MANAGE_MOVEMENT; LDA #$0; STA TICK_COUNTER ; Reset TICK_COUNTER LDA LED_COPY; EOR #$1 ; Toggle LED STA LED_COPY; STA LED; MANAGE_MOVEMENT: ; ***** Manage movement of Charles Bronson carrier ; In this section we need to update character's X and Y position ; Character's position is stored in X_OFFSET and Y_OFFSET variable groups ; First we need to see if TRIP_TIME and TRIP_COUNTERS have run out ; Check and see if TRIP_TIME is 0 LDA TRIP_TIME; CMP #$0; BNE UPDATE_OFFSETS; ; Check to see if TRIP_NUMBER = LAST_TRIP LDA TRIP_COUNTER; CMP #LAST_TRIP; BNE NEXT_TRIP; ; We need to reset TRIP_NUMBER LDA #$0; STA TRIP_COUNTER; JSR INITIALIZE_TRIP; JMP UPDATE_OFFSETS; NEXT_TRIP: CLC; ADC #$1; increment TRIP_NUMBER STA TRIP_COUNTER; JSR INITIALIZE_TRIP; UPDATE_OFFSETS: CLC; LDA X_STEP_FRACTIONAL; ADC X_OFFSET_FRACTIONAL; STA X_OFFSET_FRACTIONAL; LDA X_STEP; ADC X_OFFSET; STA X_OFFSET; LDA X_STEP_MSb; ADC X_OFFSET_MSb; AND #$1; STA X_OFFSET_MSb; CLC; LDA Y_STEP_FRACTIONAL; ADC Y_OFFSET_FRACTIONAL; STA Y_OFFSET_FRACTIONAL; LDA Y_STEP; ADC Y_OFFSET; STA Y_OFFSET; LDA Y_STEP_MSb; ADC Y_OFFSET_MSb; AND #$1; STA Y_OFFSET_MSb; ; Now we update the positions of each sprite that makes up the character LDX #$3; Initialize to 4 because first sprite in character is sprite 4 UPDATE_POS_LOOP: CLC; LDA X_INIT_LO, X; ADC X_OFFSET; STA X_LO, X; LDA X_INIT_HI, X; ADC X_OFFSET_MSb; ROR; We do this to store bit[0] into the carry bit. LDA X_HI, X; AND #$FE; ADC #$0; we add restore bit[0] from carry bit STA X_HI, X; CLC; LDA Y_INIT_LO, X; ADC Y_OFFSET; STA Y_LO, X; LDA Y_INIT_HI, X; ADC Y_OFFSET_MSb; ROR; LDA Y_HI, X; AND #$FE; ADC #$0; STA Y_HI, X; INX; CPX #18; BNE UPDATE_POS_LOOP; ; decrement TRIP_TIME SEC; LDA TRIP_TIME; SBC #$1; STA TRIP_TIME; EXIT_INTERRUPT: ; Pull registers from stack PLA; TAY; pull y PLA; TAX; pull x PLA; pull accumlator PLP; pull status RTI; COLORS_LO: .db $0, $1, $ED, $ff; palette 0 .db $0, $d0, $a8, $ff; palette 1 .db $0, $0, $0, $0; palette 2 .db $0, $0, $0, $0; palette 3 COLORS_HI: .db $0, $0, $1, $1; palette 0 .db $0, $0, $1, $1; palette 1 .db $0, $0, $0, $0; palette 2 .db $0, $0, $0, $0; palette 3 SPRITE_COLORS_LO: .db $0, $0, $6d, $ff, $0, $01, $c0, $ed .db $0, $a8, $f8, $ff, $0, $03, $bf, $77 SPRITE_COLORS_HI: .db $0, $0, $1, $1, $0, $1, $1, $1 .db $0, $1, $1, $1, $0, $0, $0, $1 SPR_PAT_A .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 .db $00, $00, $00, $40, $90, $a4, $a4, $a4, $a4, $a4, $a4, $90, $40, $00, $00, $00 ; forearm .db $00, $00, $00, $00, $00, $00, $50, $00, $00, $00, $00, $00, $00, $00, $00, $00 ;gun .db $50, $90, $a4, $a4, $a9, $a9, $a9, $a9, $a9, $a9, $a9, $a9, $a9, $a9, $a9, $a9 ;legs .db $a9, $a9, $a9, $a9, $55, $00, $00, $80, $00, $00, $00, $00, $00, $00, $00, $00 ;feet .db $00, $40, $90, $90, $40, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ; shoulder .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ;arm .db $50, $a4, $a9, $a9, $a9, $a9, $a9, $a9, $a9, $a9, $a9, $a9, $a4, $90, $40, $00 ; chest .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ; waist .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ; leg fin .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ; arm fin .db $00, $00, $00, $00, $00, $00, $00, $80, $80, $a0, $20, $28, $08, $0a, $02, $02 ; spike .db $00, $00, $00, $00, $c0, $c0, $c0, $c0, $f0, $f0, $c0, $c0, $c0, $00, $00, $00 ; head .db $00, $40, $80, $40, $10, $10, $10, $10, $04, $04, $10, $10, $10, $40, $40, $00 ; hair SPR_PAT_B .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 .db $00, $00, $00, $55, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $55, $00, $00, $00 ; forearm .db $00, $00, $00, $00, $00, $00, $55, $55, $00, $00, $00, $00, $00, $00, $00, $00 ; gun .db $55, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa ; legs .db $aa, $aa, $aa, $aa, $55, $56, $55, $55, $00, $00, $00, $00, $00, $00, $00, $00 ; feet .db $55, $aa, $aa, $aa, $aa, $55, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ; shoulder .db $00, $00, $00, $00, $00, $55, $aa, $aa, $55, $00, $00, $00, $00, $00, $00, $00 ; arm .db $55, $a6, $aa, $aa, $a5, $a6, $6a, $aa, $aa, $aa, $aa, $9a, $a6, $aa, $55, $00 ; chest .db $a9, $a9, $a9, $a4, $a4, $a4, $a4, $a4, $a4, $a4, $a4, $00, $00, $00, $00, $00 ; waist .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ; leg fin .db $55, $a8, $50, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ; arm fin .db $00, $20, $20, $28, $08, $0a, $02, $02, $00, $00, $00, $00, $00, $00, $00, $00 ; spike .db $00, $00, $00, $3f, $ff, $ff, $ff, $ff, $ff, $ff, $ff, $ff, $f5, $ff, $fc, $fc ; head .db $96, $66, $99, $80, $00, $15, $40, $05, $10, $00, $55, $40, $00, $00, $55, $04 ; hair SPR_PAT_C .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 .db $00, $00, $00, $15, $1a, $1a, $1a, $1a, $1a, $1a, $1a, $1a, $15, $00, $00, $00 ;forearm .db $00, $00, $00, $00, $00, $00, $55, $55, $50, $40, $00, $00, $00, $00, $00, $00 ;gun .db $55, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa ;legs .db $aa, $aa, $aa, $aa, $55, $01, $01, $01, $00, $00, $00, $00, $00, $00, $00, $00 ;feet .db $15, $6a, $aa, $aa, $6a, $15, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ;shoulder .db $90, $a4, $a4, $a4, $90, $95, $aa, $aa, $55, $00, $00, $00, $00, $00, $00, $00 ;arm .db $55, $9a, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $aa, $55, $00 ;chest .db $6a, $1a, $1a, $1a, $6a, $6a, $6a, $1a, $06, $06, $06, $00, $00, $00, $00, $00 ;waist .db $00, $00, $01, $09, $19, $19, $69, $69, $a9, $a9, $a9, $a9, $a9, $a9, $a9, $55 ; leg fin .db $55, $6a, $6a, $65, $50, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ; arm fin .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ; spike .db $00, $00, $00, $00, $00, $03, $0f, $ff, $ff, $ff, $df, $d7, $ff, $3f, $3f, $3f ; head .db $99, $66, $aa, $99, $aa, $68, $a0, $00, $00, $00, $00, $00, $00, $44, $41, $40 ; hair SPR_PAT_D .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ; forearm .db $00, $00, $00, $00, $00, $00, $55, $55, $55, $55, $00, $00, $00, $00, $00, $00 ;gun .db $05, $06, $1a, $1a, $6a, $6a, $6a, $6a, $6a, $6a, $6a, $6a, $6a, $6a, $6a, $6a ; legs .db $6a, $6a, $6a, $6a, $69, $64, $50, $40, $00, $00, $00, $00, $00, $00, $00, $00 ;feet .db $00, $00, $01, $01, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ;shoulder .db $6a, $6a, $6a, $6a, $1a, $1a, $1a, $06, $01, $00, $00, $00, $00, $00, $00, $00 ;arm .db $05, $1a, $6a, $6a, $6a, $6a, $6a, $6a, $6a, $6a, $6a, $6a, $1a, $06, $01, $00 ;chest .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ;waist .db $00, $00, $00, $00, $00, $00, $00, $00, $01, $01, $06, $06, $1a, $1a, $6a, $55 ; leg fin .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ; arm fin .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 ; spike .db $00, $00, $00, $00, $00, $00, $00, $00, $03, $03, $0f, $03, $00, $00, $00, $00 ; head .db $05, $1a, $66, $69, $9a, $a6, $6a, $1a, $a8, $68, $a0, $68, $15, $01, $00, $00 ; hair PATTERN_LO: .db $0, $0, $0, $0, $0, $0, $0, $0 .db $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF .db $55, $55, $55, $55, $55, $55, $55, $55 .db $0, $0, $0, $0, $0, $0, $0, $0 .db $65, $65, $65, $65, $65, $65, $65, $65 PATTERN_HI: .db $0, $0, $0, $0, $0, $0, $0, $0 .db $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF .db $55, $55, $55, $55, $55, $55, $55, $55 .db $0, $0, $0, $3, $0, $0, $0, $0 .db $5a, $5a, $5a, $5a, $5a, $5a, $5a, $5a X_INIT_LO: .db $0, $0, $0, CHAR_XPOS, CHAR_XPOS, ARM_FIN_XPOS, SPIKE_XPOS, SPIKE_XPOS .db ARM_FIN_XPOS, LEG_FIN_XPOS, FOREARM_XPOS, GUN_XPOS, CHAR_XPOS, CHAR_XPOS, CHAR_XPOS, ARM_XPOS .db CHAR_XPOS, CHAR_XPOS, $0, $0, $0, $0, $0, $0 X_INIT_HI: .db $0, $0, $0, HAIR, HEAD, ARM_FIN, SPIKE, SPIKE .db ARM_FIN, LEG_FIN, FOREARM, GUN, LEGS, FEET, SHOULDER, ARM .db CHEST, WAIST, $0, $0, $0, $0, $0, $0 Y_INIT_LO: .db $0, $0, $0, HEAD_YPOS, HEAD_YPOS, ARM_YPOS-11, SPIKE1_YPOS, SPIKE2_YPOS .db ARM_FIN_YPOS, LEG_FIN_YPOS, ARM_YPOS, ARM_YPOS, LEG_YPOS, FEET_YPOS, CHEST_YPOS, ARM_YPOS .db CHEST_YPOS, WAIST_YPOS, $0, $0, $0, $0, $0, $0 Y_INIT_HI: .db $0, $0, $0, $08, $18, $1C, $18, $1C .db $18, $18, $28, $08, $28, $28, $20, $38 .db $28, $38, $0, $0, $0, $0, $0, $0 X_STEP_FRAC_LIST: .db $0, $0, $0, $80, $80, $80, $0, $80 .db $ff, $ff, $0, $0, $0, $0, $0, $0 X_STEP_LIST: .db $1, $1, $0, $ff, $ff, $ff, $0, $ff .db $ff, $ff, $0, $0, $0, $0, $0, $0 X_STEP_MSb_LIST: .db $0, $0, $0, $1, $1, $1, $0, $1 .db $1, $1, $0, $0, $0, $0, $0, $0 Y_STEP_FRAC_LIST: .db $0, $80, $80, $0, $0, $80, $0, $80 .db $0, $ff, $ff, $0, $0, $0, $0, $0 Y_STEP_LIST: .db $0, $ff, $0, $1, $0, $ff, $ff, $0 .db $1, $ff, $ff, $0, $0, $0, $0, $0 Y_STEP_MSb_LIST: .db $0, $1, $0, $0, $0, $1, $1, $0 .db $0, $1, $1, $0, $0, $0, $0, $0 TRIP_TIME_LIST: .db 100, 50, 80, 40, 100, 80, 55, 80 .db 6, 6, 60, 0, 0, 0, 0, 0 BUILDING_START_LIST: .db 25, 24, 24, 25, 23, 23,24, 26 .db 25, 25 .db 27, 24, 24, 23, 23, 24, 24, 26 .db 25, 25, 27, 23, 23, 25, 24, 22 .db 22, 24, 26, 25, 25, 23, 23, 25 .db 25, 27, 26, 25, 28, 28, 28, 28 STAR_LSB_LIST: .db $78, $6c, $3f, $4b, $d1, $0, $a, $8a .db $b9, $a4, $4e, $55, $de, $f1, $0, $0 STAR_MSB_LIST: .db $60, $60, $61, $61, $61, $62, $62, $62 .db $62, $62, $63, $63, $63, $63, $0, $0 .org $FFFA ;first of the three vectors starts here .dw NMI ;when an NMI happens (once per frame if enabled) the ;processor will jump to the label NMI: .dw RESET ;when the processor first turns on or is reset, it will jump ;to the label RESET: .dw 0 ;external interrupt IRQ is not used in this tutorial ; ****** Website for converting BIN File to HEX ************** ; https://tomeko.net/online_tools/file_to_hex.php?lang=en
The rest of us can jump ahead to the video. In this final demo we have a mechanized Charles Bronson flying above a futuristic city. If such a game were to exist it would have to be called.....
Death Wish 2000
Before we go I would like to thank you, not only for reading this blog, but also for tolerating the pixel art which can be generously described as fair to middling at best.
If you've missed the previous blogs in the series, they are linked below.
OrangeCrab Gaming Console - Part 1
Top Comments