From a04994c267f1591caa0ca3a9e354522ebd707196 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Thu, 22 Jan 2026 15:13:56 -0600 Subject: [PATCH] dreamcast2/example: add cube_ta_fullscreen_textured_punch_through.cpp --- ...e_ta_fullscreen_textured_punch_through.cpp | 503 ++++++++++++++++++ dreamcast2/example/example.mk | 7 + .../texture/ground_slam_128x128.rgb1555 | Bin 0 -> 32768 bytes 3 files changed, 510 insertions(+) create mode 100644 dreamcast2/example/cube_ta_fullscreen_textured_punch_through.cpp create mode 100644 dreamcast2/example/texture/ground_slam_128x128.rgb1555 diff --git a/dreamcast2/example/cube_ta_fullscreen_textured_punch_through.cpp b/dreamcast2/example/cube_ta_fullscreen_textured_punch_through.cpp new file mode 100644 index 0000000..82522cd --- /dev/null +++ b/dreamcast2/example/cube_ta_fullscreen_textured_punch_through.cpp @@ -0,0 +1,503 @@ +#include "memorymap.hpp" + +#include "holly/core/object_list_bits.hpp" +#include "holly/core/region_array.hpp" +#include "holly/core/region_array_bits.hpp" +#include "holly/core/parameter_bits.hpp" +#include "holly/core/parameter.hpp" +#include "holly/ta/global_parameter.hpp" +#include "holly/ta/vertex_parameter.hpp" +#include "holly/ta/parameter_bits.hpp" +#include "holly/holly.hpp" +#include "holly/holly_bits.hpp" + +#include "sh7091/sh7091.hpp" +#include "sh7091/pref.hpp" +#include "sh7091/store_queue_transfer.hpp" + +void transfer_background_polygon(uint32_t isp_tsp_parameter_start) +{ + using namespace holly::core::parameter; + + using parameter = isp_tsp_parameter<3>; + + volatile parameter * polygon = (volatile parameter *)&texture_memory32[isp_tsp_parameter_start]; + + polygon->isp_tsp_instruction_word = isp_tsp_instruction_word::depth_compare_mode::always + | isp_tsp_instruction_word::culling_mode::no_culling; + + polygon->tsp_instruction_word = tsp_instruction_word::src_alpha_instr::one + | tsp_instruction_word::dst_alpha_instr::zero + | tsp_instruction_word::fog_control::no_fog; + + polygon->texture_control_word = 0; + + polygon->vertex[0].x = 0.0f; + polygon->vertex[0].y = 0.0f; + polygon->vertex[0].z = 0.00001f; + polygon->vertex[0].base_color = 0xff00ff; + + polygon->vertex[1].x = 32.0f; + polygon->vertex[1].y = 0.0f; + polygon->vertex[1].z = 0.00001f; + polygon->vertex[1].base_color = 0xff00ff; + + polygon->vertex[2].x = 32.0f; + polygon->vertex[2].y = 32.0f; + polygon->vertex[2].z = 0.00001f; + polygon->vertex[2].base_color = 0xff00ff; +} + +static inline uint32_t transfer_ta_global_end_of_list(uint32_t store_queue_ix) +{ + using namespace holly::ta; + using namespace holly::ta::parameter; + + // + // TA "end of list" global transfer + // + volatile global_parameter::end_of_list * end_of_list = (volatile global_parameter::end_of_list *)&store_queue[store_queue_ix]; + store_queue_ix += (sizeof (global_parameter::end_of_list)); + + end_of_list->parameter_control_word = parameter_control_word::para_type::end_of_list; + + // start store queue transfer of `end_of_list` to the TA + pref(end_of_list); + + return store_queue_ix; +} + +static inline uint32_t transfer_ta_global_polygon(uint32_t store_queue_ix, uint32_t texture_address) +{ + using namespace holly::core::parameter; + using namespace holly::ta; + using namespace holly::ta::parameter; + + // + // TA polygon global transfer + // + + volatile global_parameter::polygon_type_0 * polygon = (volatile global_parameter::polygon_type_0 *)&store_queue[store_queue_ix]; + store_queue_ix += (sizeof (global_parameter::polygon_type_0)); + + polygon->parameter_control_word = parameter_control_word::para_type::polygon_or_modifier_volume + | parameter_control_word::list_type::punch_through + | parameter_control_word::col_type::packed_color + | parameter_control_word::texture; + + polygon->isp_tsp_instruction_word = isp_tsp_instruction_word::depth_compare_mode::greater + | isp_tsp_instruction_word::culling_mode::no_culling; + // Note that it is not possible to use + // ISP_TSP_INSTRUCTION_WORD::GOURAUD_SHADING in this isp_tsp_instruction_word, + // because `gouraud` is one of the bits overwritten by the value in + // parameter_control_word. See DCDBSysArc990907E.pdf page 200. + + polygon->tsp_instruction_word = tsp_instruction_word::src_alpha_instr::src_alpha + | tsp_instruction_word::dst_alpha_instr::inverse_src_alpha + | tsp_instruction_word::fog_control::no_fog + | tsp_instruction_word::filter_mode::point_sampled + | tsp_instruction_word::texture_shading_instruction::decal + | tsp_instruction_word::texture_u_size::_128 + | tsp_instruction_word::texture_v_size::_128; + + polygon->texture_control_word = texture_control_word::pixel_format::argb1555 + | texture_control_word::scan_order::non_twiddled + | texture_control_word::texture_address(texture_address / 8); + + // start store queue transfer of `polygon` to the TA + pref(polygon); + + return store_queue_ix; +} + +static inline uint32_t transfer_ta_vertex_triangle(uint32_t store_queue_ix, + float ax, float ay, float az, float au, float av, uint32_t ac, + float bx, float by, float bz, float bu, float bv, uint32_t bc, + float cx, float cy, float cz, float cu, float cv, uint32_t cc) +{ + using namespace holly::ta; + using namespace holly::ta::parameter; + + // + // TA polygon vertex transfer + // + + volatile vertex_parameter::polygon_type_3 * vertex = (volatile vertex_parameter::polygon_type_3 *)&store_queue[store_queue_ix]; + store_queue_ix += (sizeof (vertex_parameter::polygon_type_3)) * 3; + + // bottom left + vertex[0].parameter_control_word = parameter_control_word::para_type::vertex_parameter; + vertex[0].x = ax; + vertex[0].y = ay; + vertex[0].z = az; + vertex[0].u = au; + vertex[0].v = av; + vertex[0].base_color = ac; + vertex[0].offset_color = 0; + + // start store queue transfer of `vertex[0]` to the TA + pref(&vertex[0]); + + // top center + vertex[1].parameter_control_word = parameter_control_word::para_type::vertex_parameter; + vertex[1].x = bx; + vertex[1].y = by; + vertex[1].z = bz; + vertex[1].u = bu; + vertex[1].v = bv; + vertex[1].base_color = bc; + vertex[1].offset_color = 0; + + // start store queue transfer of `vertex[1]` to the TA + pref(&vertex[1]); + + // bottom right + vertex[2].parameter_control_word = parameter_control_word::para_type::vertex_parameter + | parameter_control_word::end_of_strip; + vertex[2].x = cx; + vertex[2].y = cy; + vertex[2].z = cz; + vertex[2].u = cu; + vertex[2].v = cv; + vertex[2].base_color = cc; + vertex[2].offset_color = 0; + + // start store queue transfer of `params[2]` to the TA + pref(&vertex[2]); + + return store_queue_ix; +} + +/* + These vertex and face definitions are a trivial transformation of the default + Blender cube, as exported by the .obj exporter (with triangulation enabled). + */ +struct vec3 { + float x; + float y; + float z; +}; + +struct vec2 { + float u; + float v; +}; + +static const vec3 cube_vertex_position[] = { + { 1.0f, 1.0f, -1.0f }, + { 1.0f, -1.0f, -1.0f }, + { 1.0f, 1.0f, 1.0f }, + { 1.0f, -1.0f, 1.0f }, + { -1.0f, 1.0f, -1.0f }, + { -1.0f, -1.0f, -1.0f }, + { -1.0f, 1.0f, 1.0f }, + { -1.0f, -1.0f, 1.0f }, +}; + +static const vec2 cube_vertex_texture[] = { + { 1.0f, 0.0f }, + { 0.0f, 1.0f }, + { 0.0f, 0.0f }, + { 1.0f, 1.0f }, +}; + +struct position_texture { + int position; + int texture; +}; + +struct face { + position_texture a; + position_texture b; + position_texture c; +}; + +/* + It is also possible to submit each cube face as a 4-vertex triangle strip, or + submit the entire cube as a single triangle strip. + + Separate 3-vertex triangles are chosen to make this example more + straightforward, but this is not the best approach if high performance is + desired. + */ +static const face cube_faces[] = { + {{4, 0}, {2, 1}, {0, 2}}, + {{2, 0}, {7, 1}, {3, 2}}, + {{6, 0}, {5, 1}, {7, 2}}, + {{1, 0}, {7, 1}, {5, 2}}, + {{0, 0}, {3, 1}, {1, 2}}, + {{4, 0}, {1, 1}, {5, 2}}, + {{4, 0}, {6, 3}, {2, 1}}, + {{2, 0}, {6, 3}, {7, 1}}, + {{6, 0}, {4, 3}, {5, 1}}, + {{1, 0}, {3, 3}, {7, 1}}, + {{0, 0}, {2, 3}, {3, 1}}, + {{4, 0}, {0, 3}, {1, 1}}, +}; +static const int cube_faces_length = (sizeof (cube_faces)) / (sizeof (cube_faces[0])); + +#define cos(n) __builtin_cosf(n) +#define sin(n) __builtin_sinf(n) + +static float theta = 0; + +static inline vec3 vertex_rotate(vec3 v) +{ + // to make the cube's appearance more interesting, rotate the vertex on two + // axes + + float x0 = v.x; + float y0 = v.y; + float z0 = v.z; + + float x1 = x0 * cos(theta) - z0 * sin(theta); + float y1 = y0; + float z1 = x0 * sin(theta) + z0 * cos(theta); + + float x2 = x1; + float y2 = y1 * cos(theta) - z1 * sin(theta); + float z2 = y1 * sin(theta) + z1 * cos(theta); + + return (vec3){x2, y2, z2}; +} + +static inline vec3 vertex_perspective_divide(vec3 v) +{ + float w = 1.0f / (v.z + 3.0f); + return (vec3){v.x * w, v.y * w, w}; +} + +static inline vec3 vertex_screen_space(vec3 v) +{ + return (vec3){ + v.x * 240.f + 320.f, + v.y * 240.f + 240.f, + v.z, + }; +} + +void transfer_ta_cube(uint32_t texture_address) +{ + { + using namespace sh7091; + using sh7091::sh7091; + + // set the store queue destination address to the TA Polygon Converter FIFO + sh7091.CCN.QACR0 = sh7091::ccn::qacr0::address(ta_fifo_polygon_converter); + sh7091.CCN.QACR1 = sh7091::ccn::qacr1::address(ta_fifo_polygon_converter); + } + + uint32_t store_queue_ix = 0; + + store_queue_ix = transfer_ta_global_polygon(store_queue_ix, texture_address); + + for (int face_ix = 0; face_ix < cube_faces_length; face_ix++) { + int ipa = cube_faces[face_ix].a.position; + int ipb = cube_faces[face_ix].b.position; + int ipc = cube_faces[face_ix].c.position; + + vec3 vpa = vertex_screen_space( + vertex_perspective_divide( + vertex_rotate(cube_vertex_position[ipa]))); + + vec3 vpb = vertex_screen_space( + vertex_perspective_divide( + vertex_rotate(cube_vertex_position[ipb]))); + + vec3 vpc = vertex_screen_space( + vertex_perspective_divide( + vertex_rotate(cube_vertex_position[ipc]))); + + int ita = cube_faces[face_ix].a.texture; + int itb = cube_faces[face_ix].b.texture; + int itc = cube_faces[face_ix].c.texture; + + vec2 vta = cube_vertex_texture[ita]; + vec2 vtb = cube_vertex_texture[itb]; + vec2 vtc = cube_vertex_texture[itc]; + + // vertex color is irrelevant in "decal" mode + uint32_t va_color = 0; + uint32_t vb_color = 0; + uint32_t vc_color = 0; + + store_queue_ix = transfer_ta_vertex_triangle(store_queue_ix, + vpa.x, vpa.y, vpa.z, vta.u, vta.v, va_color, + vpb.x, vpb.y, vpb.z, vtb.u, vtb.v, vb_color, + vpc.x, vpc.y, vpc.z, vtc.u, vtc.v, vc_color); + } + + store_queue_ix = transfer_ta_global_end_of_list(store_queue_ix); +} + +const uint8_t texture[] __attribute__((aligned(4))) = { + #embed "texture/ground_slam_128x128.rgb1555" +}; + +void transfer_texture(uint32_t texture_start) +{ + // use 4-byte transfers to texture memory, for slightly increased transfer + // speed + // + // It would be even faster to use the SH4 store queue for this operation, or + // SH4 DMA. + + sh7091::store_queue_transfer::copy((void *)&texture_memory64[texture_start], texture, (sizeof (texture))); +} + +void main() +{ + /* + a very simple memory map: + + the ordering within texture memory is not significant, and could be + anything + */ + uint32_t framebuffer_start = 0x200000; // intentionally the same address that the boot rom used to draw the SEGA logo + uint32_t isp_tsp_parameter_start = 0x400000; + uint32_t region_array_start = 0x500000; + uint32_t object_list_start = 0x100000; + + // these addresses are in "64-bit" texture memory address space: + uint32_t texture_start = 0x700000; + + const int tile_y_num = 480 / 32; + const int tile_x_num = 640 / 32; + + using namespace holly::core; + + region_array::list_block_size list_block_size = { + .translucent = 8 * 4, + }; + + region_array::transfer(tile_x_num, + tile_y_num, + list_block_size, + region_array_start, + object_list_start); + + transfer_background_polygon(isp_tsp_parameter_start); + + ////////////////////////////////////////////////////////////////////////////// + // transfer the texture image to texture ram + ////////////////////////////////////////////////////////////////////////////// + + transfer_texture(texture_start); + + ////////////////////////////////////////////////////////////////////////////// + // configure the TA + ////////////////////////////////////////////////////////////////////////////// + + using namespace holly; + using holly::holly; + + // TA_GLOB_TILE_CLIP restricts which "object pointer blocks" are written + // to. + // + // This can also be used to implement "windowing", as long as the desired + // window size happens to be a multiple of 32 pixels. The "User Tile Clip" TA + // control parameter can also ~equivalently be used as many times as desired + // within a single TA initialization to produce an identical effect. + // + // See DCDBSysArc990907E.pdf page 183. + holly.TA_GLOB_TILE_CLIP = ta_glob_tile_clip::tile_y_num(tile_y_num - 1) + | ta_glob_tile_clip::tile_x_num(tile_x_num - 1); + + // While CORE supports arbitrary-length object lists, the TA uses "object + // pointer blocks" as a memory allocation strategy. These fixed-length blocks + // can still have infinite length via "object pointer block links". This + // mechanism is illustrated in DCDBSysArc990907E.pdf page 188. + holly.TA_ALLOC_CTRL = ta_alloc_ctrl::opb_mode::increasing_addresses + | ta_alloc_ctrl::pt_opb::_8x4byte; + + // While building object lists, the TA contains an internal index (exposed as + // the read-only TA_ITP_CURRENT) for the next address that new ISP/TSP will be + // stored at. The initial value of this index is TA_ISP_BASE. + + // reserve space in ISP/TSP parameters for the background parameter + using polygon = holly::core::parameter::isp_tsp_parameter<3>; + uint32_t ta_isp_base_offset = (sizeof (polygon)) * 1; + + holly.TA_ISP_BASE = isp_tsp_parameter_start + ta_isp_base_offset; + holly.TA_ISP_LIMIT = isp_tsp_parameter_start + 0x100000; + + // Similarly, the TA also contains, for up to 600 tiles, an internal index for + // the next address that an object list entry will be stored for each + // tile. These internal indicies are partially exposed via the read-only + // TA_OL_POINTERS. + holly.TA_OL_BASE = object_list_start; + + // TA_OL_LIMIT, DCDBSysArc990907E.pdf page 385: + // + // > Because the TA may automatically store data in the address that is + // > specified by this register, it must not be used for other data. For + // > example, the address specified here must not be the same as the address + // > in the TA_ISP_BASE register. + holly.TA_OL_LIMIT = object_list_start + 0x100000 - 32; + + ////////////////////////////////////////////////////////////////////////////// + // configure CORE + ////////////////////////////////////////////////////////////////////////////// + + // REGION_BASE is the (texture memory-relative) address of the region array. + holly.REGION_BASE = region_array_start; + + // PARAM_BASE is the (texture memory-relative) address of ISP/TSP parameters. + // Anything that references an ISP/TSP parameter does so relative to this + // address (and not relative to the beginning of texture memory). + holly.PARAM_BASE = isp_tsp_parameter_start; + + // Set the offset of the background ISP/TSP parameter, relative to PARAM_BASE + // SKIP is related to the size of each vertex + uint32_t background_offset = 0; + + holly.ISP_BACKGND_T = isp_backgnd_t::tag_address(background_offset / 4) + | isp_backgnd_t::tag_offset(0) + | isp_backgnd_t::skip(1); + + // FB_W_SOF1 is the (texture memory-relative) address of the framebuffer that + // will be written to when a tile is rendered/flushed. + holly.FB_W_SOF1 = framebuffer_start; + + // without waiting for rendering to actually complete, immediately display the + // framebuffer. + holly.FB_R_SOF1 = framebuffer_start; + + // draw 500 frames of cube rotation + for (int i = 0; i < 500; i++) { + ////////////////////////////////////////////////////////////////////////////// + // transfer cube to texture memory via the TA polygon converter FIFO + ////////////////////////////////////////////////////////////////////////////// + + // TA_LIST_INIT needs to be written (every frame) prior to the first FIFO + // write. + holly.TA_LIST_INIT = ta_list_init::list_init; + + // dummy TA_LIST_INIT read; DCDBSysArc990907E.pdf in multiple places says this + // step is required. + (void)holly.TA_LIST_INIT; + + transfer_ta_cube(texture_start); + + ////////////////////////////////////////////////////////////////////////////// + // wait for vertical synchronization (and the TA) + ////////////////////////////////////////////////////////////////////////////// + + while (!(spg_status::vsync(holly.SPG_STATUS))); + while (spg_status::vsync(holly.SPG_STATUS)); + + ////////////////////////////////////////////////////////////////////////////// + // start the actual rasterization + ////////////////////////////////////////////////////////////////////////////// + + // start the actual render--the rendering process begins by interpreting the + // region array + holly.STARTRENDER = 1; + + // increment theta for the cube rotation animation + // (used by the `vertex_rotate` function) + theta += 0.01f; + } + + // return from main; this will effectively jump back to the serial loader +} diff --git a/dreamcast2/example/example.mk b/dreamcast2/example/example.mk index 359742b..ac6a34d 100644 --- a/dreamcast2/example/example.mk +++ b/dreamcast2/example/example.mk @@ -52,6 +52,13 @@ CUBE_TA_FULLSCREEN_TEXTURED_OBJ = \ example/cube_ta_fullscreen_textured.elf: LDSCRIPT = $(LIB)/main.lds example/cube_ta_fullscreen_textured.elf: $(START_OBJ) $(CUBE_TA_FULLSCREEN_TEXTURED_OBJ) +CUBE_TA_FULLSCREEN_TEXTURED_PUNCH_THROUGH_OBJ = \ + holly/core/region_array.o \ + example/cube_ta_fullscreen_textured_punch_through.o + +example/cube_ta_fullscreen_textured_punch_through.elf: LDSCRIPT = $(LIB)/main.lds +example/cube_ta_fullscreen_textured_punch_through.elf: $(START_OBJ) $(CUBE_TA_FULLSCREEN_TEXTURED_PUNCH_THROUGH_OBJ) + SUZANNE_TRIANGLE_STRIPS_OBJ = \ holly/core/region_array.o \ example/suzanne_triangle_strips.o diff --git a/dreamcast2/example/texture/ground_slam_128x128.rgb1555 b/dreamcast2/example/texture/ground_slam_128x128.rgb1555 new file mode 100644 index 0000000000000000000000000000000000000000..07c2b4283ffd9c08c4231314338648c6f09e1b18 GIT binary patch literal 32768 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

0&)*q? z*=Ppt6}HhimMrJ-5!Y%_olLSArQb;Cwt#t#%oA1qQPx`5ry2G+{Dbqk{DaGVjmg|c zju3S&M)Wpn#5>-({NtXDdJda#(Noyhwak?$Y-TNM&tFyRJ8xhEmTjJHSg3t2f7NrR)Ei6pHTI3I&*h)%TYuiq+$o!Z6FyhVlftti$(C7En>?}@ zp`YZ<);=cipVg>0mdPYW~>tEz_ z1gFY>R!?>+UBRDPbD1|XN6kIQ`S-VEUgbPb-pG+E@DHghKK1!(wHd09JI_)(I$p