Project F

Castle Drawing

Published · Updated

In this FPGA demo, we use multiple shapes (rectangles, triangles, circles) to render a simple picture of a castle. We don’t use any software or CPU, just shape rasterization and finite state machines. This Verilog design runs on the Digilent Arty A7 or as a Verilator/SDL simulation on your computer.

Castle Verilator Simulation

Building the Demo

Find the Verilog source and build instructions in the projf-explore git repo:
https://github.com/projf/projf-explore/tree/main/demos/castle-drawing

To learn about hardware drawing, check out my tutorial on Lines and Triangles and 2D Shapes.

Demo Structure

Castle Rendering

The castle render module is a mix of rectangles, triangles and circles drawn with a finite state machine. The background sky and grass are not drawn into the framebuffer but are rendered directly based on the screen position (see next section).

module render_castle #(
    parameter CORDW=16,  // signed coordinate width (bits)
    parameter CIDXW=4,   // colour index width (bits)
    parameter SCALE=1    // drawing scale: 1=320x180, 2=640x360, 4=1280x720
    ) (
    input  wire logic clk,    // clock
    input  wire logic rst,    // reset
    input  wire logic oe,     // output enable
    input  wire logic start,  // start drawing
    output      logic signed [CORDW-1:0] x,  // horizontal draw position
    output      logic signed [CORDW-1:0] y,  // vertical draw position
    output      logic [CIDXW-1:0] cidx,  // pixel colour
    output      logic drawing,  // actively drawing
    output      logic done      // drawing is complete (high for one tick)
    );

    localparam RBOW_CNT=8;    // number of shapes in rainbow
    localparam SHAPE_CNT=19;  // number of shapes in castle
    logic [$clog2(RBOW_CNT)-1:0] rbow_id;    // rainbow shape identifier
    logic [$clog2(SHAPE_CNT)-1:0] shape_id;  // castle shape identifier

    logic signed [CORDW-1:0] vx0, vy0, vx1, vy1, vx2, vy2;  // shape coords
    logic signed [CORDW-1:0] vr0;  // circle radius
    logic signed [CORDW-1:0] x_tri,    y_tri;     // triangle framebuffer coords
    logic signed [CORDW-1:0] x_rect,   y_rect;    // rectangle framebuffer coords
    logic signed [CORDW-1:0] x_circle, y_circle;  // circle framebuffer coords
    logic draw_done;  // combined done signal
    logic draw_start_tri, drawing_tri, draw_done_tri;           // drawing triangle
    logic draw_start_rect, drawing_rect, draw_done_rect;        // drawing rectangle
    logic draw_start_circle, drawing_circle, draw_done_circle;  // drawing circle

    // draw state machine
    enum {IDLE, INIT_RBOW, DRAW_RBOW, INIT_CASTLE, DRAW_CASTLE, DONE} state;
    always_ff @(posedge clk) begin
        case (state)
            INIT_RBOW: begin
                state <= DRAW_RBOW;
                draw_start_circle <= 1;
                vx0 <= 368;
                vy0 <= 160;
                vr0 <= 180 - 5 * rbow_id;
                cidx <= (rbow_id == RBOW_CNT-1) ? 'h0 : 'h2 + rbow_id;
            end
            DRAW_RBOW: begin
                draw_start_circle <= 0;
                if (draw_done) begin
                    if (rbow_id == RBOW_CNT-1) begin
                        state <= INIT_CASTLE;
                    end else begin
                        rbow_id <= rbow_id + 1;
                        state <= INIT_RBOW;
                    end
                end
            end
            INIT_CASTLE: begin  // register coordinates and colour (Sweetie 16 palette)
                state <= DRAW_CASTLE;
                case (shape_id)
                    'd0: begin  // main building
                        draw_start_rect <= 1;
                        vx0 <=  60; vy0 <=  75;
                        vx1 <= 190; vy1 <= 130;
                        cidx <= 'hE;  // dark grey
                    end
                    'd1: begin  // drawbridge
                        draw_start_rect <= 1;
                        vx0 <= 110; vy0 <= 110;
                        vx1 <= 140; vy1 <= 130;
                        cidx <= 'hF;  // blue-grey
                    end
                    'd2: begin  // drawbridge arch
                        draw_start_circle <= 1;
                        vx0 <= 125; vy0 <= 110;
                        vr0 <=  15;
                        cidx <= 'hF;  // blue-grey
                    end
                    'd3: begin  // left tower
                        draw_start_rect <= 1;
                        vx0 <=  40; vy0 <=  50;
                        vx1 <=  60; vy1 <= 130;
                        cidx <= 'hE;  // dark grey
                    end
                    'd4: begin  // middle tower
                        draw_start_rect <= 1;
                        vx0 <= 110; vy0 <=  45;
                        vx1 <= 140; vy1 <=  75;
                        cidx <= 'hE;  // dark grey
                    end
                    'd5: begin  // right tower
                        draw_start_rect <= 1;
                        vx0 <= 190; vy0 <=  50;
                        vx1 <= 210; vy1 <= 130;
                        cidx <= 'hE;  // dark grey
                    end
                    'd6: begin  // left roof
                        draw_start_tri <= 1;
                        vx0 <=  50; vy0 <=  35;
                        vx1 <=  65; vy1 <=  50;
                        vx2 <=  35; vy2 <=  50;
                        cidx <= 'h2;  // red
                    end
                    'd7: begin  // middle roof
                        draw_start_tri <= 1;
                        vx0 <= 125; vy0 <=  25;
                        vx1 <= 145; vy1 <=  45;
                        vx2 <= 105; vy2 <=  45;
                        cidx <= 'h2;  // red
                    end
                    'd8: begin  // right roof
                        draw_start_tri <= 1;
                        vx0 <= 200; vy0 <=  35;
                        vx1 <= 215; vy1 <=  50;
                        vx2 <= 185; vy2 <=  50;
                        cidx <= 'h2;  // red
                    end
                    'd9: begin  // left window
                        draw_start_rect <= 1;
                        vx0 <=  46; vy0 <=  55;
                        vx1 <=  54; vy1 <=  70;
                        cidx <= 'hF;  // blue-grey
                    end
                    'd10: begin  // middle window
                        draw_start_rect <= 1;
                        vx0 <= 120; vy0 <=  50;
                        vx1 <= 130; vy1 <=  70;
                        cidx <= 'hF;  // blue-grey
                    end
                    'd11: begin  // right window
                        draw_start_rect <= 1;
                        vx0 <= 196; vy0 <=  55;
                        vx1 <= 204; vy1 <=  70;
                        cidx <= 'hF;  // blue-grey
                    end
                    'd12: begin  // battlement 1
                        draw_start_rect <= 1;
                        vx0 <=  63; vy0 <=  67;
                        vx1 <=  72; vy1 <=  75;
                        cidx <= 'hE;  // dark grey
                    end
                    'd13: begin  // battlement 2
                        draw_start_rect <= 1;
                        vx0 <=   80; vy0 <=  67;
                        vx1 <=   89; vy1 <=  75;
                        cidx <= 'hE;  // dark grey
                    end
                    'd14: begin  // battlement 3
                        draw_start_rect <= 1;
                        vx0 <=  97; vy0 <=  67;
                        vx1 <= 106; vy1 <=  75;
                        cidx <= 'hE;  // dark grey
                    end
                    'd15: begin  // battlement 4
                        draw_start_rect <= 1;
                        vx0 <= 144; vy0 <=  67;
                        vx1 <= 153; vy1 <=  75;
                        cidx <= 'hE;  // dark grey
                    end
                    'd16: begin  // battlement 5
                        draw_start_rect <= 1;
                        vx0 <= 161; vy0 <=  67;
                        vx1 <= 170; vy1 <=  75;
                        cidx <= 'hE;  // dark grey
                    end
                    'd17: begin  // battlement 6
                        draw_start_rect <= 1;
                        vx0 <= 178; vy0 <=  67;
                        vx1 <= 187; vy1 <=  75;
                        cidx <= 'hE;  // dark grey
                    end
                    default: begin  // clear ground
                        draw_start_rect <= 1;
                        vx0 <=   0; vy0 <= 131;
                        vx1 <= 319; vy1 <= 179;
                        cidx <= 'h0;  // background (transparent)
                    end
                endcase
            end
            DRAW_CASTLE: begin
                draw_start_tri    <= 0;
                draw_start_rect   <= 0;
                draw_start_circle <= 0;
                if (draw_done) begin
                    if (shape_id == SHAPE_CNT-1) begin
                        state <= DONE;
                    end else begin
                        shape_id <= shape_id + 1;
                        state <= INIT_CASTLE;
                    end
                end
            end
            DONE: state <= DONE;
            default: if (start) state <= INIT_RBOW;  // IDLE
        endcase
        if (rst) state <= IDLE;
    end

    draw_triangle_fill #(.CORDW(CORDW)) draw_triangle_inst (
        .clk,
        .rst,
        .start(draw_start_tri),
        .oe,
        .x0(vx0 * SCALE),
        .y0(vy0 * SCALE),
        .x1(vx1 * SCALE),
        .y1(vy1 * SCALE),
        .x2(vx2 * SCALE),
        .y2(vy2 * SCALE),
        .x(x_tri),
        .y(y_tri),
        .drawing(drawing_tri),
        .busy(),
        .done(draw_done_tri)
    );

    draw_rectangle_fill #(.CORDW(CORDW)) draw_rectangle_inst (
        .clk,
        .rst,
        .start(draw_start_rect),
        .oe,
        .x0(vx0 * SCALE),
        .y0(vy0 * SCALE),
        .x1(vx1 * SCALE),
        .y1(vy1 * SCALE),
        .x(x_rect),
        .y(y_rect),
        .drawing(drawing_rect),
        .busy(),
        .done(draw_done_rect)
    );

    draw_circle_fill #(.CORDW(CORDW)) draw_circle_inst (
        .clk,
        .rst,
        .start(draw_start_circle),
        .oe,
        .x0(vx0 * SCALE),
        .y0(vy0 * SCALE),
        .r0(vr0 * SCALE),
        .x(x_circle),
        .y(y_circle),
        .drawing(drawing_circle),
        .busy(),
        .done(draw_done_circle)
    );

    // output coordinates for the active shape
    always_ff @(posedge clk) begin
        x <= drawing_tri ? x_tri : (drawing_rect ? x_rect : x_circle);
        y <= drawing_tri ? y_tri : (drawing_rect ? y_rect : y_circle);
    end

    // drawing and done apply to all drawing types
    always_ff @(posedge clk) begin
        drawing   <= drawing_tri   || drawing_rect   || drawing_circle;
        draw_done <= draw_done_tri || draw_done_rect || draw_done_circle;
    end

    // doesn't need delaying one cycle because it's based on draw_done
    always_ff @(posedge clk) done <= (state == DONE);
endmodule

Rendering Background

The background is rendered in the top module but will move to its own module in time.

    // background colour (sy ignores 16:9 letterbox)
    logic [COLRW-1:0] bg_colr;
    always_ff @(posedge clk_pix) begin
        if (line) begin
            if      (sy ==   0) bg_colr <= 12'h000;
            else if (sy ==  60) bg_colr <= 12'h239;
            else if (sy == 140) bg_colr <= 12'h24A;
            else if (sy == 195) bg_colr <= 12'h25B;
            else if (sy == 230) bg_colr <= 12'h26C;
            else if (sy == 260) bg_colr <= 12'h27D;
            else if (sy == 285) bg_colr <= 12'h29E;
            else if (sy == 305) bg_colr <= 12'h2BF;
            else if (sy == 322) bg_colr <= 12'h370;  // below castle (2x pix)
            else if (sy == 420) bg_colr <= 12'h000;
        end
    end

What’s Next?

Check out my other FPGA demos or the FPGA graphics tutorials.

Get in touch on Mastodon, Bluesky, or X. If you enjoy my work, please sponsor me. 🙏