From 55b993e08e9e44b04090df1d33fd632f6a64cfbf Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 13 Jun 2026 10:15:47 -0600 Subject: [PATCH] Add aurora::gx::dl reader & optimizer --- cmake/aurora_gx.cmake | 2 + extern/CMakeLists.txt | 4 +- include/aurora/dl.hpp | 138 +++++++++++ include/dolphin/gd/GDBase.h | 2 +- include/dolphin/gx/GXAurora.h | 37 +-- include/dolphin/gx/GXCommandList.h | 2 +- lib/dolphin/gd/GDAurora.cpp | 6 +- lib/dolphin/gd/GDGeometry.cpp | 2 +- lib/dolphin/gx/GXAurora.cpp | 10 +- lib/dolphin/gx/GXExtra.cpp | 6 +- lib/dolphin/gx/GXGeometry.cpp | 2 +- lib/dolphin/gx/GXTexture.cpp | 4 +- lib/dolphin/gx/GXVert.cpp | 2 +- lib/dolphin/gx/__gx.h | 2 +- lib/gx/attr_fmt.cpp | 110 +++++++++ lib/gx/command_processor.cpp | 90 +++++--- lib/gx/dl.cpp | 357 +++++++++++++++++++++++++++++ lib/gx/gx.cpp | 100 -------- tests/CMakeLists.txt | 4 + tests/gx_dl_test.cpp | 286 +++++++++++++++++++++++ tests/gx_fifo_test.cpp | 32 +-- tests/gx_test_stubs.cpp | 8 +- 22 files changed, 1026 insertions(+), 180 deletions(-) create mode 100644 include/aurora/dl.hpp create mode 100644 lib/gx/attr_fmt.cpp create mode 100644 lib/gx/dl.cpp create mode 100644 tests/gx_dl_test.cpp diff --git a/cmake/aurora_gx.cmake b/cmake/aurora_gx.cmake index e81b621..a473bd0 100644 --- a/cmake/aurora_gx.cmake +++ b/cmake/aurora_gx.cmake @@ -11,7 +11,9 @@ add_library(aurora_gx STATIC lib/gfx/texture_format.cpp lib/gfx/texture_convert.cpp lib/gfx/texture_replacement.cpp + lib/gx/attr_fmt.cpp lib/gx/command_processor.cpp + lib/gx/dl.cpp lib/gx/fifo.cpp lib/gx/gx.cpp lib/gx/pipeline.cpp diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt index 8603f5f..cc28d11 100644 --- a/extern/CMakeLists.txt +++ b/extern/CMakeLists.txt @@ -308,8 +308,8 @@ if (NOT TARGET TracyClient) FetchContent_Declare( tracy - URL https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz - URL_HASH SHA256=24d342b5127d7f659dc3cf94f24347b348cc736f25b17a691fd7f69541937658 + URL https://github.com/wolfpld/tracy/archive/6789e7d6f9a65ec98926b602097a33a9676d2606.tar.gz + URL_HASH SHA256=ebfe4fb50d7c254901979355c80a7d4cd33624aa2ec0fe90b3b238153cd5d69b DOWNLOAD_EXTRACT_TIMESTAMP TRUE EXCLUDE_FROM_ALL ) diff --git a/include/aurora/dl.hpp b/include/aurora/dl.hpp new file mode 100644 index 0000000..263c5f8 --- /dev/null +++ b/include/aurora/dl.hpp @@ -0,0 +1,138 @@ +#pragma once + +#include + +#include +#include +#include + +namespace aurora::gx::dl { + +struct VtxLayout { + struct Attr { + u8 offset = 0; + u8 size = 0; + GXAttrType type = GX_NONE; + }; + Attr attrs[GX_VA_MAX_ATTR]{}; + u8 stride = 0; +}; + +using VtxFmtLists = std::array; + +struct DrawCmd { + GXPrimitive prim; + GXVtxFmt fmt; + u16 vtxCount; + const u8* vertices; + const VtxLayout* layout; + const u8* indices; // DrawIndexed only: native-endian u16s + u32 indexCount; + + const u8* vtx(u32 vtx) const { return vertices + vtx * layout->stride; } + /** Read an indexed attribute (GX_INDEX8 / GX_INDEX16) or a GX_DIRECT MTXIDX. */ + u16 attr_idx(u32 vtx, GXAttr attr) const; + /** Read one prebuilt index (DrawIndexed only). */ + u16 index(u32 i) const; +}; + +struct Command { + enum class Kind { + Draw, // GX draw prim + DrawIndexed, // GX_AURORA_DRAW_INDEXED + Passthrough, // everything else + }; + Kind kind; + const u8* data; // full command bytes, including the opcode + u32 size; + DrawCmd draw; // if Draw or DrawIndexed +}; + +/** + * Parses a big-endian GX display list command by command. + * + * Vertex layouts are computed per vertex format from the desc list. + * GX_VA_*MTXIDX attributes are always one byte. + * Any other GX_DIRECT attribute requires a GXVtxAttrFmtList for the referenced + * GX_VTXFMT; if not present, the reader will return failure. + */ +class Reader { +public: + Reader(const u8* dl, u32 size, const GXVtxDescList* desc, const VtxFmtLists* fmts = nullptr); + /** + * Stride-only reader for callers that know the vertex stride but not the + * attribute format. DrawCmd::attr_idx will always return 0. + */ + Reader(const u8* dl, u32 size, u8 stride); + + std::optional next(); + bool failed() const { return mFailed; } + u32 pos() const { return mPos; } + +private: + const u8* mData; + u32 mSize; + u32 mPos = 0; + const GXVtxDescList* mDesc = nullptr; + const VtxFmtLists* mFmts = nullptr; + bool mFailed = false; + std::optional mLayouts[GX_MAX_VTXFMT]; + bool mLayoutComputed[GX_MAX_VTXFMT] = {}; + + const VtxLayout* layout(GXVtxFmt fmt); +}; + +template +bool expand_triangles(GXPrimitive prim, u16 vtxCount, F&& f) { + switch (prim) { + case GX_TRIANGLES: + if (vtxCount < 3 || vtxCount % 3 != 0) { + return false; + } + for (u16 v = 0; v < vtxCount; v += 3) { + f(v, static_cast(v + 1), static_cast(v + 2)); + } + return true; + case GX_TRIANGLESTRIP: + if (vtxCount < 3) { + return false; + } + for (u16 v = 2; v < vtxCount; ++v) { + if ((v & 1) == 0) { + f(static_cast(v - 2), static_cast(v - 1), v); + } else { + f(static_cast(v - 1), static_cast(v - 2), v); + } + } + return true; + case GX_TRIANGLEFAN: + if (vtxCount < 3) { + return false; + } + for (u16 v = 2; v < vtxCount; ++v) { + f(0, static_cast(v - 1), v); + } + return true; + case GX_QUADS: + if (vtxCount < 4 || vtxCount % 4 != 0) { + return false; + } + for (u16 v = 0; v < vtxCount; v += 4) { + f(v, static_cast(v + 1), static_cast(v + 2)); + f(static_cast(v + 2), static_cast(v + 3), v); + } + return true; + default: + return false; + } +} + +/** + * Rewrite a display list, merging adjacent triangulable draws into prebuilt GX_AURORA_DRAW_INDEXED commands. + * State commands are passed through untouched, NOPs are dropped. + * Returns nullopt if the display list contains anything unsupported. + */ +std::optional> optimize(const u8* dl, u32 size, const GXVtxDescList* desc, + const VtxFmtLists* fmts = nullptr); + +} // namespace aurora::gx::dl diff --git a/include/dolphin/gd/GDBase.h b/include/dolphin/gd/GDBase.h index 1c88333..b25732e 100644 --- a/include/dolphin/gd/GDBase.h +++ b/include/dolphin/gd/GDBase.h @@ -142,7 +142,7 @@ inline static void GDWriteBPCmd(u32 regval) { } inline static void GDWriteAuroraCmd(u16 subCommand) { - GDWrite_u8(GX_LOAD_AURORA); + GDWrite_u8(GX_AURORA); GDWrite_u16(subCommand); } diff --git a/include/dolphin/gx/GXAurora.h b/include/dolphin/gx/GXAurora.h index 8f88a93..72e584b 100644 --- a/include/dolphin/gx/GXAurora.h +++ b/include/dolphin/gx/GXAurora.h @@ -8,58 +8,58 @@ extern "C" { #endif // -// Subcommands for GX_LOAD_AURORA. +// Subcommands for GX_AURORA. // /** * Sets the actual render viewport in native framebuffer coordinates. * Must be followed by six f32 values: left, top, width, height, nearz, farz. */ -#define GX_LOAD_AURORA_VIEWPORT_RENDER 0x0001 +#define GX_AURORA_LOAD_VIEWPORT_RENDER 0x0001 /** * Sets the actual render scissor in native framebuffer coordinates. * Must be followed by four u32 values: left, top, width, height. */ -#define GX_LOAD_AURORA_SCISSOR_RENDER 0x0002 +#define GX_AURORA_LOAD_SCISSOR_RENDER 0x0002 /** * Aurora equivalent of CP_REG_ARRAYBASE_ID: sets the base address and size of a vertex array. * This command must be followed by a 64-bit memory address, 32-bit size, and 1-byte little-endian flag. * The index of the vertex array is given by the lowest 4 bits of the command ID, - * e.g. writing GX_LOAD_AURORA_ARRAYBASE + 5 will set the vertex array for the sixth vertex attribute. + * e.g. writing GX_AURORA_LOAD_ARRAYBASE + 5 will set the vertex array for the sixth vertex attribute. * To set strides, use the normal CP_REG_ARRAYSTRIDE_ID register. */ -#define GX_LOAD_AURORA_ARRAYBASE 0x0010 +#define GX_AURORA_LOAD_ARRAYBASE 0x0010 /** * Pushes a debug group to the backend graphics API. These may show in debugging tools such as RenderDoc. * Must be followed by a u16 string length and that many UTF-8 characters (no null terminator required). * It is considered an error to have unpopped debug groups at the end of the frame. They will be automatically cleared. */ -#define GX_LOAD_AURORA_DEBUG_GROUP_PUSH 0x0020 +#define GX_AURORA_DEBUG_GROUP_PUSH 0x0020 /** * Pops a previously pushed debug group. * Followed by nothing. */ -#define GX_LOAD_AURORA_DEBUG_GROUP_POP 0x0021 +#define GX_AURORA_DEBUG_GROUP_POP 0x0021 /** * Sends a debug marker to the backend graphics API. * Must be followed by a u16 string length and that many UTF-8 characters (no null terminator required). */ -#define GX_LOAD_AURORA_DEBUG_MARKER_INSERT 0x0022 +#define GX_AURORA_DEBUG_MARKER_INSERT 0x0022 -#define GX_LOAD_AURORA_TEXOBJ 0x0030 +#define GX_AURORA_LOAD_TEXOBJ 0x0030 -#define GX_LOAD_AURORA_TLUT 0x0031 +#define GX_AURORA_LOAD_TLUT 0x0031 -#define GX_LOAD_AURORA_DESTROY_TEXOBJ 0x0032 +#define GX_AURORA_DESTROY_TEXOBJ 0x0032 -#define GX_LOAD_AURORA_DESTROY_TLUT 0x0033 +#define GX_AURORA_DESTROY_TLUT 0x0033 -#define GX_LOAD_AURORA_DESTROY_COPY_TEX 0x0034 +#define GX_AURORA_DESTROY_COPY_TEX 0x0034 /** * Draw primitives with the vertex count derived from a byte length, as written by @@ -67,7 +67,16 @@ extern "C" { * a u32 vertex data byte length, then that many bytes of vertex data. The byte length * must be a whole multiple of the current vertex size or zero (no draw). */ -#define GX_LOAD_AURORA_DRAW_SIZED 0x0040 +#define GX_AURORA_DRAW_SIZED 0x0040 + +/** + * Draw pre-merged triangles with a prebuilt index buffer, as written by the display + * list optimizer (aurora::gx::dl::optimize). Must be followed by a u8 draw opcode + * (vtxfmt | GX_TRIANGLES), a u16 vertex count, a u32 index count, that many u16 + * indices, then vertex count * vertex size bytes of packed vertex data. Index data + * is always host-endian regardless of stream endianness. + */ +#define GX_AURORA_DRAW_INDEXED 0x0041 #define GX2_SET_POLYGON_OFFSET 0x1000 diff --git a/include/dolphin/gx/GXCommandList.h b/include/dolphin/gx/GXCommandList.h index 069a086..bab5396 100644 --- a/include/dolphin/gx/GXCommandList.h +++ b/include/dolphin/gx/GXCommandList.h @@ -29,7 +29,7 @@ extern "C" { * Custom Aurora commands that are followed by another 2-byte identifier. * See GXAurora.h for further documentation on these. */ -#define GX_LOAD_AURORA 0x50 +#define GX_AURORA 0x50 #define GX_OPCODE_MASK 0xF8 #define GX_VAT_MASK 0x07 diff --git a/lib/dolphin/gd/GDAurora.cpp b/lib/dolphin/gd/GDAurora.cpp index 14bbbee..996c294 100644 --- a/lib/dolphin/gd/GDAurora.cpp +++ b/lib/dolphin/gd/GDAurora.cpp @@ -18,15 +18,15 @@ static void GDWriteString(const char* label) { } void GDPushDebugGroup(const char* label) { - GDWriteAuroraCmd(GX_LOAD_AURORA_DEBUG_GROUP_PUSH); + GDWriteAuroraCmd(GX_AURORA_DEBUG_GROUP_PUSH); GDWriteString(label); } void GDPopDebugGroup() { - GDWriteAuroraCmd(GX_LOAD_AURORA_DEBUG_GROUP_POP); + GDWriteAuroraCmd(GX_AURORA_DEBUG_GROUP_POP); } void GDInsertDebugMarker(const char* label) { - GDWriteAuroraCmd(GX_LOAD_AURORA_DEBUG_MARKER_INSERT); + GDWriteAuroraCmd(GX_AURORA_DEBUG_MARKER_INSERT); GDWriteString(label); } diff --git a/lib/dolphin/gd/GDGeometry.cpp b/lib/dolphin/gd/GDGeometry.cpp index 194a1b5..9013c91 100644 --- a/lib/dolphin/gd/GDGeometry.cpp +++ b/lib/dolphin/gd/GDGeometry.cpp @@ -255,7 +255,7 @@ void GDSetArraySized(GXAttr attr, void* base_ptr, u32 size, u8 stride, bool le) assert((cpAttr & ~0xF) == 0); - GDWriteAuroraCmd(cpAttr + GX_LOAD_AURORA_ARRAYBASE); + GDWriteAuroraCmd(cpAttr + GX_AURORA_LOAD_ARRAYBASE); GDWrite_u64((u64)base_ptr); GDWrite_u32(size); GDWrite_u8(le ? 1 : 0); diff --git a/lib/dolphin/gx/GXAurora.cpp b/lib/dolphin/gx/GXAurora.cpp index 833ed96..1485f0f 100644 --- a/lib/dolphin/gx/GXAurora.cpp +++ b/lib/dolphin/gx/GXAurora.cpp @@ -22,14 +22,14 @@ static void GXWriteString(const char* label) { } void GXPushDebugGroup(const char* label) { - GX_WRITE_AURORA(GX_LOAD_AURORA_DEBUG_GROUP_PUSH); + GX_WRITE_AURORA(GX_AURORA_DEBUG_GROUP_PUSH); GXWriteString(label); } -void GXPopDebugGroup() { GX_WRITE_AURORA(GX_LOAD_AURORA_DEBUG_GROUP_POP); } +void GXPopDebugGroup() { GX_WRITE_AURORA(GX_AURORA_DEBUG_GROUP_POP); } void GXInsertDebugMarker(const char* label) { - GX_WRITE_AURORA(GX_LOAD_AURORA_DEBUG_MARKER_INSERT); + GX_WRITE_AURORA(GX_AURORA_DEBUG_MARKER_INSERT); GXWriteString(label); } @@ -49,7 +49,7 @@ void AuroraGetRenderSize(u32* width, u32* height) { } void GXSetViewportRender(f32 left, f32 top, f32 wd, f32 ht, f32 nearz, f32 farz) { - GX_WRITE_AURORA(GX_LOAD_AURORA_VIEWPORT_RENDER); + GX_WRITE_AURORA(GX_AURORA_LOAD_VIEWPORT_RENDER); GX_WRITE_F32(left); GX_WRITE_F32(top); GX_WRITE_F32(wd); @@ -59,7 +59,7 @@ void GXSetViewportRender(f32 left, f32 top, f32 wd, f32 ht, f32 nearz, f32 farz) } void GXSetScissorRender(u32 left, u32 top, u32 wd, u32 ht) { - GX_WRITE_AURORA(GX_LOAD_AURORA_SCISSOR_RENDER); + GX_WRITE_AURORA(GX_AURORA_LOAD_SCISSOR_RENDER); GX_WRITE_U32(left); GX_WRITE_U32(top); GX_WRITE_U32(wd); diff --git a/lib/dolphin/gx/GXExtra.cpp b/lib/dolphin/gx/GXExtra.cpp index 8cb2b59..a508d2a 100644 --- a/lib/dolphin/gx/GXExtra.cpp +++ b/lib/dolphin/gx/GXExtra.cpp @@ -6,7 +6,7 @@ extern "C" { void GXDestroyTexObj(GXTexObj* obj_) { auto* obj = reinterpret_cast(obj_); if (obj->texObjId != 0) { - GX_WRITE_AURORA(GX_LOAD_AURORA_DESTROY_TEXOBJ); + GX_WRITE_AURORA(GX_AURORA_DESTROY_TEXOBJ); GX_WRITE_U32(obj->texObjId); } obj->texObjId = 0; @@ -15,7 +15,7 @@ void GXDestroyTexObj(GXTexObj* obj_) { void GXDestroyTlutObj(GXTlutObj* obj_) { auto* obj = reinterpret_cast(obj_); if (obj->tlutObjId != 0) { - GX_WRITE_AURORA(GX_LOAD_AURORA_DESTROY_TLUT); + GX_WRITE_AURORA(GX_AURORA_DESTROY_TLUT); GX_WRITE_U32(obj->tlutObjId); } obj->tlutObjId = 0; @@ -23,7 +23,7 @@ void GXDestroyTlutObj(GXTlutObj* obj_) { void GXDestroyCopyTex(void* dest) { if (dest != nullptr) { - GX_WRITE_AURORA(GX_LOAD_AURORA_DESTROY_COPY_TEX); + GX_WRITE_AURORA(GX_AURORA_DESTROY_COPY_TEX); GX_WRITE_U64(reinterpret_cast(dest)); } } diff --git a/lib/dolphin/gx/GXGeometry.cpp b/lib/dolphin/gx/GXGeometry.cpp index 4e3f698..fc0e27e 100644 --- a/lib/dolphin/gx/GXGeometry.cpp +++ b/lib/dolphin/gx/GXGeometry.cpp @@ -225,7 +225,7 @@ void GXSetArray(GXAttr attr, const void* data, u32 size, u8 stride, bool le) { assert((cpIdx & ~0xF) == 0); // Write array base - GX_WRITE_AURORA(GX_LOAD_AURORA_ARRAYBASE | cpIdx); + GX_WRITE_AURORA(GX_AURORA_LOAD_ARRAYBASE | cpIdx); GX_WRITE_U64(reinterpret_cast(data)); GX_WRITE_U32(size); GX_WRITE_U8(le ? 1 : 0); diff --git a/lib/dolphin/gx/GXTexture.cpp b/lib/dolphin/gx/GXTexture.cpp index 1564911..37ba24a 100644 --- a/lib/dolphin/gx/GXTexture.cpp +++ b/lib/dolphin/gx/GXTexture.cpp @@ -79,7 +79,7 @@ void init_texobj_common(GXTexObj_& obj, const void* data, u16 width, u16 height, } void emit_loaded_texobj_metadata(const GXTexObj_& obj, GXTexMapID id) { - GX_WRITE_AURORA(GX_LOAD_AURORA_TEXOBJ); + GX_WRITE_AURORA(GX_AURORA_LOAD_TEXOBJ); GX_WRITE_U8(static_cast(id)); GX_WRITE_U64(reinterpret_cast(obj.data)); GX_WRITE_U32(obj.width()); @@ -92,7 +92,7 @@ void emit_loaded_texobj_metadata(const GXTexObj_& obj, GXTexMapID id) { } void emit_loaded_tlut_metadata(const GXTlutObj_& obj, u32 idx) { - GX_WRITE_AURORA(GX_LOAD_AURORA_TLUT); + GX_WRITE_AURORA(GX_AURORA_LOAD_TLUT); GX_WRITE_U8(static_cast(idx)); GX_WRITE_U64(reinterpret_cast(obj.data)); GX_WRITE_U32(static_cast(obj.format)); diff --git a/lib/dolphin/gx/GXVert.cpp b/lib/dolphin/gx/GXVert.cpp index a604388..8d11b91 100644 --- a/lib/dolphin/gx/GXVert.cpp +++ b/lib/dolphin/gx/GXVert.cpp @@ -29,7 +29,7 @@ void GXBegin(GXPrimitive primitive, GXVtxFmt vtxFmt, u16 nVerts) { sBeginAuto = nVerts == GX_AUTO; if (sBeginAuto) { ASSERT(!aurora::gx::fifo::in_display_list(), "GXBegin: GX_AUTO not supported in display lists"); - GX_WRITE_AURORA(GX_LOAD_AURORA_DRAW_SIZED); + GX_WRITE_AURORA(GX_AURORA_DRAW_SIZED); GX_WRITE_U8(drawCmd); sBeginSizeOffset = aurora::gx::fifo::get_buffer_size(); GX_WRITE_U32(0); diff --git a/lib/dolphin/gx/__gx.h b/lib/dolphin/gx/__gx.h index 9189346..5be1395 100644 --- a/lib/dolphin/gx/__gx.h +++ b/lib/dolphin/gx/__gx.h @@ -72,7 +72,7 @@ #define GX_WRITE_AURORA(cmd) \ do { \ - GX_WRITE_U8(GX_LOAD_AURORA); \ + GX_WRITE_U8(GX_AURORA); \ GX_WRITE_U16(cmd); \ } while (0) diff --git a/lib/gx/attr_fmt.cpp b/lib/gx/attr_fmt.cpp new file mode 100644 index 0000000..0115dca --- /dev/null +++ b/lib/gx/attr_fmt.cpp @@ -0,0 +1,110 @@ +#include "gx.hpp" + +#include "../internal.hpp" +#include "gx_fmt.hpp" + +static aurora::Module Log("aurora::gx"); + +namespace aurora::gx { + +u8 comp_type_size(GXAttr attr, GXCompType type) noexcept { + switch (attr) { + case GX_VA_PNMTXIDX: + case GX_VA_TEX0MTXIDX: + case GX_VA_TEX1MTXIDX: + case GX_VA_TEX2MTXIDX: + case GX_VA_TEX3MTXIDX: + case GX_VA_TEX4MTXIDX: + case GX_VA_TEX5MTXIDX: + case GX_VA_TEX6MTXIDX: + case GX_VA_TEX7MTXIDX: + return 1; + case GX_VA_CLR0: + case GX_VA_CLR1: + switch (type) { + case GX_RGB565: + case GX_RGBA4: + return 2; + case GX_RGB8: + case GX_RGBA6: + return 3; + case GX_RGBX8: + case GX_RGBA8: + return 4; + } + default: + switch (type) { + case GX_U8: + case GX_S8: + return 1; + case GX_U16: + case GX_S16: + return 2; + case GX_F32: + return 4; + default: + Log.fatal("comp_type_size: Unsupported component type {}", type); + } + } +} + +u8 comp_cnt_count(GXAttr attr, GXCompCnt cnt) noexcept { + switch (attr) { + case GX_VA_PNMTXIDX: + case GX_VA_TEX0MTXIDX: + case GX_VA_TEX1MTXIDX: + case GX_VA_TEX2MTXIDX: + case GX_VA_TEX3MTXIDX: + case GX_VA_TEX4MTXIDX: + case GX_VA_TEX5MTXIDX: + case GX_VA_TEX6MTXIDX: + case GX_VA_TEX7MTXIDX: + return 1; + case GX_VA_POS: + switch (cnt) { + case GX_POS_XY: + return 2; + case GX_POS_XYZ: + return 3; + default: + break; + } + break; + case GX_VA_NRM: + switch (cnt) { + case GX_NRM_XYZ: + return 3; + case GX_NRM_NBT: + case GX_NRM_NBT3: + return 9; + default: + break; + } + break; + case GX_VA_CLR0: + case GX_VA_CLR1: + return 1; + case GX_VA_TEX0: + case GX_VA_TEX1: + case GX_VA_TEX2: + case GX_VA_TEX3: + case GX_VA_TEX4: + case GX_VA_TEX5: + case GX_VA_TEX6: + case GX_VA_TEX7: + switch (cnt) { + case GX_TEX_S: + return 1; + case GX_TEX_ST: + return 2; + default: + break; + } + break; + default: + break; + } + Log.fatal("comp_cnt_count: Unsupported attr/cnt {} {}", attr, cnt); +} + +} // namespace aurora::gx diff --git a/lib/gx/command_processor.cpp b/lib/gx/command_processor.cpp index 9748556..21e177d 100644 --- a/lib/gx/command_processor.cpp +++ b/lib/gx/command_processor.cpp @@ -443,7 +443,7 @@ void process(const u8* data, u32 size, bool bigEndian) { break; } - case GX_LOAD_AURORA: { + case GX_AURORA: { handle_aurora(data, pos, size, bigEndian); break; } @@ -1241,7 +1241,7 @@ static void handle_cp(u8 addr, u32 value, bool bigEndian) { } // Array base addresses (0xA0-0xAF) else if (addr >= 0xA0 && addr <= 0xAF) { - Log.error("CP_REG_ARRAYBASE_ID is not supported on Aurora. Use GX_LOAD_AURORA_ARRAYBASE instead."); + Log.error("CP_REG_ARRAYBASE_ID is not supported on Aurora. Use GX_AURORA_LOAD_ARRAYBASE instead."); } // Array strides (0xB0-0xBF) else if (addr >= 0xB0 && addr <= 0xBF) { @@ -1538,6 +1538,8 @@ static u32 calculate_last_vtx_size(GXVtxFmt fmt) { } static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, gfx::Range vertRange); +static void push_gx_draw(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, gfx::Range vertRange, gfx::Range idxRange, + u32 numIndices); // Draw command handler - parses vertices inline and caches results static ByteBuffer handle_draw_idx_buf; @@ -1626,6 +1628,11 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, g realBuf.clear(); } + push_gx_draw(prim, fmt, vtxCount, vertRange, idxRange, numIndices); +} + +static void push_gx_draw(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, gfx::Range vertRange, gfx::Range idxRange, + u32 numIndices) { // Build pipeline, bind groups, and push draw command BindGroupRanges ranges{}; for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) { @@ -1688,8 +1695,8 @@ void handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) { pos += 2; // Setting of vertex array bases. - if (subCmd == GX_LOAD_AURORA_VIEWPORT_RENDER) { - CHECK(pos + 24 <= size, "GX_LOAD_AURORA_VIEWPORT_RENDER read overrun"); + if (subCmd == GX_AURORA_LOAD_VIEWPORT_RENDER) { + CHECK(pos + 24 <= size, "GX_AURORA_LOAD_VIEWPORT_RENDER read overrun"); const f32 left = read_f32(data + pos, bigEndian); pos += 4; const f32 top = read_f32(data + pos, bigEndian); @@ -1710,8 +1717,8 @@ void handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) { .znear = nearZ, .zfar = farZ, }); - } else if (subCmd == GX_LOAD_AURORA_SCISSOR_RENDER) { - CHECK(pos + 16 <= size, "GX_LOAD_AURORA_SCISSOR_RENDER read overrun"); + } else if (subCmd == GX_AURORA_LOAD_SCISSOR_RENDER) { + CHECK(pos + 16 <= size, "GX_AURORA_LOAD_SCISSOR_RENDER read overrun"); const int32_t left = static_cast(read_u32(data + pos, bigEndian)); pos += 4; const int32_t top = static_cast(read_u32(data + pos, bigEndian)); @@ -1721,9 +1728,9 @@ void handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) { const int32_t height = static_cast(read_u32(data + pos, bigEndian)); pos += 4; set_render_scissor({left, top, width, height}); - } else if (subCmd >= GX_LOAD_AURORA_ARRAYBASE && subCmd <= (GX_LOAD_AURORA_ARRAYBASE | 0x0f)) { - CHECK(pos + 13 <= size, "GX_LOAD_AURORA_ARRAYBASE read overrun"); - u32 attrIdx = subCmd - GX_LOAD_AURORA_ARRAYBASE + GX_VA_POS; + } else if (subCmd >= GX_AURORA_LOAD_ARRAYBASE && subCmd <= (GX_AURORA_LOAD_ARRAYBASE | 0x0f)) { + CHECK(pos + 13 <= size, "GX_AURORA_LOAD_ARRAYBASE read overrun"); + u32 attrIdx = subCmd - GX_AURORA_LOAD_ARRAYBASE + GX_VA_POS; u64 arrayAddr = read_u64(data + pos, bigEndian); pos += 8; @@ -1742,8 +1749,8 @@ void handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) { array.cachedRange = {}; g_gxState.stateDirty = true; } - } else if (subCmd == GX_LOAD_AURORA_TEXOBJ) { - CHECK(pos + 34 <= size, "GX_LOAD_AURORA_TEXOBJ read overrun"); + } else if (subCmd == GX_AURORA_LOAD_TEXOBJ) { + CHECK(pos + 34 <= size, "GX_AURORA_LOAD_TEXOBJ read overrun"); const auto texMapId = data[pos]; pos += 1; CHECK(texMapId < MaxTextures, "invalid texture map id {}", texMapId); @@ -1770,8 +1777,8 @@ void handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) { pos += 4; slot.set_no_cache(false); // Reset no-cache flag g_gxState.stateDirty = true; - } else if (subCmd == GX_LOAD_AURORA_TLUT) { - CHECK(pos + 23 <= size, "GX_LOAD_AURORA_TLUT read overrun"); + } else if (subCmd == GX_AURORA_LOAD_TLUT) { + CHECK(pos + 23 <= size, "GX_AURORA_LOAD_TLUT read overrun"); const auto idx = data[pos]; pos += 1; CHECK(idx < MaxTluts, "invalid tlut slot {}", idx); @@ -1801,20 +1808,20 @@ void handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) { g_gxState.clamp = read_f32(data + pos, bigEndian); pos += 4; g_gxState.stateDirty = true; - } else if (subCmd == GX_LOAD_AURORA_DESTROY_TEXOBJ) { - CHECK(pos + 4 <= size, "GX_LOAD_AURORA_DESTROY_TEXOBJ read overrun"); + } else if (subCmd == GX_AURORA_DESTROY_TEXOBJ) { + CHECK(pos + 4 <= size, "GX_AURORA_DESTROY_TEXOBJ read overrun"); evict_texture_object(read_u32(data + pos, bigEndian)); pos += 4; - } else if (subCmd == GX_LOAD_AURORA_DESTROY_TLUT) { - CHECK(pos + 4 <= size, "GX_LOAD_AURORA_DESTROY_TLUT read overrun"); + } else if (subCmd == GX_AURORA_DESTROY_TLUT) { + CHECK(pos + 4 <= size, "GX_AURORA_DESTROY_TLUT read overrun"); evict_tlut_object(read_u32(data + pos, bigEndian)); pos += 4; - } else if (subCmd == GX_LOAD_AURORA_DESTROY_COPY_TEX) { - CHECK(pos + 8 <= size, "GX_LOAD_AURORA_DESTROY_COPY_TEX read overrun"); + } else if (subCmd == GX_AURORA_DESTROY_COPY_TEX) { + CHECK(pos + 8 <= size, "GX_AURORA_DESTROY_COPY_TEX read overrun"); evict_copy_texture(reinterpret_cast(read_u64(data + pos, bigEndian))); pos += 8; - } else if (subCmd == GX_LOAD_AURORA_DRAW_SIZED) { - CHECK(pos + 5 <= size, "GX_LOAD_AURORA_DRAW_SIZED read overrun"); + } else if (subCmd == GX_AURORA_DRAW_SIZED) { + CHECK(pos + 5 <= size, "GX_AURORA_DRAW_SIZED read overrun"); u8 cmd = data[pos]; pos += 1; u32 byteLen = read_u32(data + pos, bigEndian); @@ -1829,17 +1836,48 @@ void handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) { vtxSize = calculate_last_vtx_size(fmt); } ASSERT(vtxSize != 0 && byteLen % vtxSize == 0, - "GX_LOAD_AURORA_DRAW_SIZED: {} bytes is not a whole number of size-{} vertices", byteLen, vtxSize); + "GX_AURORA_DRAW_SIZED: {} bytes is not a whole number of size-{} vertices", byteLen, vtxSize); u32 vtxCount = byteLen / vtxSize; - ASSERT(vtxCount <= 0xFFFF, "GX_LOAD_AURORA_DRAW_SIZED: too many vertices ({})", vtxCount); + ASSERT(vtxCount <= 0xFFFF, "GX_AURORA_DRAW_SIZED: too many vertices ({})", vtxCount); draw_prim(prim, fmt, static_cast(vtxCount), data, pos, size); } - } else if (subCmd == GX_LOAD_AURORA_DEBUG_GROUP_PUSH) { + } else if (subCmd == GX_AURORA_DRAW_INDEXED) { + ZoneScopedN("DRAW_INDEXED"); + CHECK(pos + 7 <= size, "GX_AURORA_DRAW_INDEXED read overrun"); + const u8 cmd = data[pos]; + pos += 1; + const u16 vtxCount = read_u16(data + pos, bigEndian); + pos += 2; + const u32 indexCount = read_u32(data + pos, bigEndian); + pos += 4; + const GXVtxFmt fmt = static_cast(cmd & CP_VAT_MASK); + const GXPrimitive prim = static_cast(cmd & CP_OPCODE_MASK); + ASSERT(prim == GX_TRIANGLES, "GX_AURORA_DRAW_INDEXED: primitive must be GX_TRIANGLES, got {}", + static_cast(prim)); + const u32 idxBytes = indexCount * static_cast(sizeof(u16)); + CHECK(pos + idxBytes <= size, "GX_AURORA_DRAW_INDEXED index data overrun"); + // Index data is always host-endian; push it to the GPU buffer as-is + const gfx::Range idxRange = gfx::push_indices(data + pos, idxBytes, 4); + pos += idxBytes; + u32 vtxSize; + if (g_gxState.lastVtxFmt == fmt) { + vtxSize = g_gxState.lastVtxSize; + } else { + vtxSize = calculate_last_vtx_size(fmt); + } + const u32 totalVtxBytes = vtxCount * vtxSize; + CHECK(pos + totalVtxBytes <= size, "GX_AURORA_DRAW_INDEXED vertex data overrun"); + const gfx::Range vertRange = gfx::push_verts(data + pos, totalVtxBytes, 4); + pos += totalVtxBytes; + if (indexCount != 0) { + push_gx_draw(prim, fmt, vtxCount, vertRange, idxRange, indexCount); + } + } else if (subCmd == GX_AURORA_DEBUG_GROUP_PUSH) { auto label = read_string(data, pos, size, bigEndian); gfx::push_debug_group(std::move(label)); - } else if (subCmd == GX_LOAD_AURORA_DEBUG_GROUP_POP) { + } else if (subCmd == GX_AURORA_DEBUG_GROUP_POP) { pop_debug_group(); - } else if (subCmd == GX_LOAD_AURORA_DEBUG_MARKER_INSERT) { + } else if (subCmd == GX_AURORA_DEBUG_MARKER_INSERT) { auto label = read_string(data, pos, size, bigEndian); gfx::insert_debug_marker(std::move(label)); } diff --git a/lib/gx/dl.cpp b/lib/gx/dl.cpp new file mode 100644 index 0000000..4b0626e --- /dev/null +++ b/lib/gx/dl.cpp @@ -0,0 +1,357 @@ +#include "aurora/dl.hpp" + +#include +#include + +#include "../internal.hpp" +#include "gx.hpp" + +namespace aurora::gx::dl { +static Module Log("aurora::gx::dl"); + +static bool is_draw_opcode(u8 opcode) { + return opcode == GX_QUADS || opcode == GX_TRIANGLES || opcode == GX_TRIANGLESTRIP || opcode == GX_TRIANGLEFAN || + opcode == GX_LINES || opcode == GX_LINESTRIP || opcode == GX_POINTS; +} + +static u16 read_be16(const u8* data) { return static_cast(data[0]) << 8 | data[1]; } + +static u32 read_be32(const u8* data) { + return static_cast(data[0]) << 24 | static_cast(data[1]) << 16 | static_cast(data[2]) << 8 | data[3]; +} + +static const GXVtxAttrFmtList* find_attr_fmt(const GXVtxAttrFmtList* list, GXAttr attr) { + if (list == nullptr) { + return nullptr; + } + for (; list->attr != GX_VA_NULL; ++list) { + if (list->attr == attr) { + return list; + } + } + return nullptr; +} + +static std::optional compute_layout(const GXVtxDescList* desc, const GXVtxAttrFmtList* fmt) { + VtxLayout out{}; + u32 stride = 0; + for (; desc->attr != GX_VA_NULL; ++desc) { + if (desc->attr >= GX_VA_MAX_ATTR) { + return std::nullopt; + } + u32 size = 0; + switch (desc->type) { + case GX_NONE: + continue; + case GX_DIRECT: { + if (desc->attr >= GX_VA_PNMTXIDX && desc->attr <= GX_VA_TEX7MTXIDX) { + size = 1; + break; + } + const auto* attrFmt = find_attr_fmt(fmt, desc->attr); + if (attrFmt == nullptr) { + return std::nullopt; + } + size = comp_type_size(desc->attr, attrFmt->type) * comp_cnt_count(desc->attr, attrFmt->cnt); + break; + } + case GX_INDEX8: + size = 1; + break; + case GX_INDEX16: + size = 2; + break; + default: + return std::nullopt; + } + if (desc->attr == GX_VA_NRM && (desc->type == GX_INDEX8 || desc->type == GX_INDEX16)) { + // GX_NRM_NBT3 normals are three separate indices + const auto* attrFmt = find_attr_fmt(fmt, GX_VA_NRM); + if (attrFmt != nullptr && attrFmt->cnt == GX_NRM_NBT3) { + size *= 3; + } + } + auto& attr = out.attrs[desc->attr]; + attr.offset = static_cast(stride); + attr.size = static_cast(size); + attr.type = desc->type; + stride += size; + if (stride > 0xFF) { + return std::nullopt; + } + } + if (stride == 0) { + return std::nullopt; + } + out.stride = static_cast(stride); + return out; +} + +u16 DrawCmd::attr_idx(u32 vtxIdx, GXAttr attr) const { + const auto& a = layout->attrs[attr]; + const u8* ptr = vertices + vtxIdx * layout->stride + a.offset; + switch (a.type) { + case GX_INDEX8: + return ptr[0]; + case GX_INDEX16: + return read_be16(ptr); + case GX_DIRECT: // *MTXIDX only + return ptr[0]; + default: + return 0; + } +} + +u16 DrawCmd::index(u32 i) const { + u16 value; + std::memcpy(&value, indices + i * sizeof(u16), sizeof(u16)); + return value; +} + +Reader::Reader(const u8* dl, u32 size, const GXVtxDescList* desc, const VtxFmtLists* fmts) +: mData(dl), mSize(size), mDesc(desc), mFmts(fmts) {} + +Reader::Reader(const u8* dl, u32 size, u8 stride) : mData(dl), mSize(size) { + VtxLayout layout{}; + layout.stride = stride; + for (u32 fmt = 0; fmt < GX_MAX_VTXFMT; ++fmt) { + mLayouts[fmt] = layout; + mLayoutComputed[fmt] = true; + } +} + +const VtxLayout* Reader::layout(GXVtxFmt fmt) { + if (!mLayoutComputed[fmt]) { + mLayoutComputed[fmt] = true; + if (mDesc != nullptr) { + mLayouts[fmt] = compute_layout(mDesc, mFmts != nullptr ? (*mFmts)[fmt] : nullptr); + } + } + return mLayouts[fmt].has_value() ? &*mLayouts[fmt] : nullptr; +} + +std::optional Reader::next() { + if (mFailed || mPos >= mSize) { + return std::nullopt; + } + + const u32 start = mPos; + const u8 cmd = mData[start]; + const u8 opcode = cmd & GX_OPCODE_MASK; + + const auto fail = [&](const char* what) -> std::optional { + Log.warn("Reader: {} (opcode 0x{:02X} at offset {})", what, cmd, start); + mFailed = true; + return std::nullopt; + }; + const auto passthrough = [&](const u32 cmdSize) -> std::optional { + if (start + cmdSize > mSize) { + return fail("command overrun"); + } + mPos = start + cmdSize; + return Command{Command::Kind::Passthrough, mData + start, cmdSize, {}}; + }; + + switch (opcode) { + case GX_NOP: + case GX_CMD_INVL_VC: + return passthrough(1); + case GX_LOAD_BP_REG & GX_OPCODE_MASK: + return passthrough(5); + case GX_LOAD_CP_REG: + return passthrough(6); + case GX_LOAD_XF_REG: { + if (start + 5 > mSize) { + return fail("XF load overrun"); + } + const u32 count = read_be16(mData + start + 1) + 1; + return passthrough(5 + count * 4); + } + case GX_LOAD_INDX_A: + case GX_LOAD_INDX_B: + case GX_LOAD_INDX_C: + case GX_LOAD_INDX_D: + return passthrough(5); + case GX_CMD_CALL_DL: + return passthrough(9); + case GX_AURORA: { + if (start + 3 > mSize) { + return fail("Aurora subcommand overrun"); + } + if (read_be16(mData + start + 1) != GX_AURORA_DRAW_INDEXED) { + return fail("unsupported Aurora subcommand"); + } + if (start + 10 > mSize) { + return fail("DRAW_INDEXED header overrun"); + } + const u8 drawCmd = mData[start + 3]; + const auto fmt = static_cast(drawCmd & GX_VAT_MASK); + const u16 vtxCount = read_be16(mData + start + 4); + const u32 indexCount = read_be32(mData + start + 6); + const VtxLayout* lo = layout(fmt); + if (lo == nullptr) { + return fail("no layout for DRAW_INDEXED vertex format"); + } + const u32 idxBytes = indexCount * sizeof(u16); + const u32 cmdSize = 10 + idxBytes + vtxCount * lo->stride; + if (start + cmdSize > mSize) { + return fail("DRAW_INDEXED data overrun"); + } + mPos = start + cmdSize; + return Command{ + Command::Kind::DrawIndexed, + mData + start, + cmdSize, + DrawCmd{ + static_cast(drawCmd & GX_OPCODE_MASK), + fmt, + vtxCount, + mData + start + 10 + idxBytes, + lo, + mData + start + 10, + indexCount, + }, + }; + } + default: { + if (!is_draw_opcode(opcode)) { + return fail("unknown opcode"); + } + if (start + 3 > mSize) { + return fail("draw header overrun"); + } + const auto fmt = static_cast(cmd & GX_VAT_MASK); + const u16 vtxCount = read_be16(mData + start + 1); + const VtxLayout* lo = layout(fmt); + if (lo == nullptr) { + return fail("no layout for draw vertex format"); + } + const u32 cmdSize = 3 + vtxCount * lo->stride; + if (start + cmdSize > mSize) { + return fail("draw data overrun"); + } + mPos = start + cmdSize; + return Command{ + Command::Kind::Draw, + mData + start, + cmdSize, + DrawCmd{ + static_cast(opcode), + fmt, + vtxCount, + mData + start + 3, + lo, + nullptr, + 0, + }, + }; + } + } +} + +namespace { + +struct DrawBatch { + GXVtxFmt fmt = GX_VTXFMT0; + u16 vtxCount = 0; + bool allTriangles = true; + std::vector verts; + std::vector indices; +}; + +void push_be16(std::vector& out, u16 value) { + out.push_back(value >> 8); + out.push_back(value & 0xFF); +} + +void push_be32(std::vector& out, u32 value) { + out.push_back(value >> 24); + out.push_back(value >> 16 & 0xFF); + out.push_back(value >> 8 & 0xFF); + out.push_back(value & 0xFF); +} + +void flush_batch(std::vector& out, DrawBatch& batch) { + if (batch.vtxCount != 0) { + if (batch.allTriangles) { + // plain triangle draw does not require an index buffer + out.push_back(static_cast(GX_TRIANGLES) | static_cast(batch.fmt)); + push_be16(out, batch.vtxCount); + } else { + out.push_back(GX_AURORA); + push_be16(out, GX_AURORA_DRAW_INDEXED); + out.push_back(static_cast(GX_TRIANGLES) | static_cast(batch.fmt)); + push_be16(out, batch.vtxCount); + push_be32(out, static_cast(batch.indices.size())); + // index data is host-endian; see GX_AURORA_DRAW_INDEXED + const auto* idxData = reinterpret_cast(batch.indices.data()); + out.insert(out.end(), idxData, idxData + batch.indices.size() * sizeof(u16)); + } + out.insert(out.end(), batch.verts.begin(), batch.verts.end()); + } + batch.vtxCount = 0; + batch.allTriangles = true; + batch.verts.clear(); + batch.indices.clear(); +} + +} // namespace + +std::optional> optimize(const u8* dl, u32 size, const GXVtxDescList* desc, const VtxFmtLists* fmts) { + Reader reader{dl, size, desc, fmts}; + std::vector out; + out.reserve(size); + DrawBatch batch; + + while (const auto cmd = reader.next()) { + const auto copy_verbatim = [&] { + flush_batch(out, batch); + out.insert(out.end(), cmd->data, cmd->data + cmd->size); + }; + + switch (cmd->kind) { + case Command::Kind::Passthrough: + if (cmd->data[0] != GX_NOP) { + copy_verbatim(); + } + break; + case Command::Kind::DrawIndexed: + copy_verbatim(); + break; + case Command::Kind::Draw: { + const auto& draw = cmd->draw; + if (batch.vtxCount != 0 && (batch.fmt != draw.fmt || static_cast(batch.vtxCount) + draw.vtxCount > 0xFFFF)) { + flush_batch(out, batch); + } + const u16 base = batch.vtxCount; + const bool expanded = expand_triangles(draw.prim, draw.vtxCount, [&](u16 i0, u16 i1, u16 i2) { + batch.indices.push_back(base + i0); + batch.indices.push_back(base + i1); + batch.indices.push_back(base + i2); + }); + if (!expanded) { + // lines/points or degenerate counts; expand_triangles validates before emitting + copy_verbatim(); + break; + } + if (batch.vtxCount == 0) { + batch.fmt = draw.fmt; + } + if (draw.prim != GX_TRIANGLES) { + batch.allTriangles = false; + } + batch.verts.insert(batch.verts.end(), draw.vertices, draw.vertices + draw.vtxCount * draw.layout->stride); + batch.vtxCount += draw.vtxCount; + break; + } + } + } + + if (reader.failed()) { + return std::nullopt; + } + flush_batch(out, batch); + return out; +} + +} // namespace aurora::gx::dl diff --git a/lib/gx/gx.cpp b/lib/gx/gx.cpp index 9ca9036..9fa5bcc 100644 --- a/lib/gx/gx.cpp +++ b/lib/gx/gx.cpp @@ -677,106 +677,6 @@ wgpu::RenderPipeline build_pipeline(const PipelineConfig& config, ArrayRef + +using namespace aurora::gx::dl; + +namespace aurora::gfx { +extern gx::DrawData g_testLastDraw; +extern uint32_t g_testDrawCount; +} // namespace aurora::gfx + +namespace { + +const GXVtxDescList kPosClrDesc[] = { + {GX_VA_POS, GX_INDEX8}, + {GX_VA_CLR0, GX_INDEX8}, + {GX_VA_NULL, GX_NONE}, +}; + +const GXVtxDescList kVtxDesc[] = { + {GX_VA_POS, GX_INDEX8}, {GX_VA_NRM, GX_INDEX8}, {GX_VA_CLR0, GX_INDEX8}, + {GX_VA_TEX0, GX_INDEX8}, {GX_VA_NULL, GX_NONE}, +}; + +u8 op(GXPrimitive prim, GXVtxFmt fmt) { return static_cast(prim) | static_cast(fmt); } + +void be16(std::vector& out, u16 value) { + out.push_back(value >> 8); + out.push_back(value & 0xFF); +} + +void draw_cmd(std::vector& out, u8 opcode, u16 vtxCount, std::initializer_list vertices) { + out.push_back(opcode); + be16(out, vtxCount); + out.insert(out.end(), vertices); +} + +u16 host_u16(const u8* data) { + u16 value; + std::memcpy(&value, data, sizeof(value)); + return value; +} + +std::vector> collect_triangles(GXPrimitive prim, u16 vtxCount) { + std::vector> tris; + expand_triangles(prim, vtxCount, [&](u16 i0, u16 i1, u16 i2) { tris.push_back({i0, i1, i2}); }); + return tris; +} + +} // namespace + +TEST(GXDlReader, WalksLeafStripDl) { + std::vector dl; + dl.push_back(GX_NOP); + // 4-vertex strip, vertices are (pos, nrm, clr, tex) index tuples + draw_cmd(dl, op(GX_TRIANGLESTRIP, GX_VTXFMT0), 4, {0, 0, 0, 0, 1, 0, 1, 1, 2, 0, 2, 2, 3, 0, 3, 3}); + dl.push_back(GX_NOP); + + Reader reader{dl.data(), static_cast(dl.size()), kVtxDesc}; + + auto cmd = reader.next(); + ASSERT_TRUE(cmd.has_value()); + EXPECT_EQ(cmd->kind, Command::Kind::Passthrough); + EXPECT_EQ(cmd->size, 1u); + + cmd = reader.next(); + ASSERT_TRUE(cmd.has_value()); + ASSERT_EQ(cmd->kind, Command::Kind::Draw); + EXPECT_EQ(cmd->draw.prim, GX_TRIANGLESTRIP); + EXPECT_EQ(cmd->draw.fmt, GX_VTXFMT0); + EXPECT_EQ(cmd->draw.vtxCount, 4); + EXPECT_EQ(cmd->draw.layout->stride, 4); + EXPECT_EQ(cmd->draw.attr_idx(0, GX_VA_POS), 0); + EXPECT_EQ(cmd->draw.attr_idx(2, GX_VA_POS), 2); + EXPECT_EQ(cmd->draw.attr_idx(2, GX_VA_CLR0), 2); + EXPECT_EQ(cmd->draw.attr_idx(3, GX_VA_TEX0), 3); + EXPECT_EQ(cmd->draw.attr_idx(3, GX_VA_NRM), 0); + + cmd = reader.next(); + ASSERT_TRUE(cmd.has_value()); + EXPECT_EQ(cmd->kind, Command::Kind::Passthrough); + + EXPECT_FALSE(reader.next().has_value()); + EXPECT_FALSE(reader.failed()); +} + +TEST(GXDlReader, FailsOnUnknownOpcode) { + const std::vector dl{0x70, 0x00, 0x00}; + Reader reader{dl.data(), static_cast(dl.size()), kPosClrDesc}; + EXPECT_FALSE(reader.next().has_value()); + EXPECT_TRUE(reader.failed()); +} + +TEST(GXDlReader, FailsOnDrawOverrun) { + std::vector dl; + draw_cmd(dl, op(GX_TRIANGLESTRIP, GX_VTXFMT0), 100, {0, 0, 1, 1}); + Reader reader{dl.data(), static_cast(dl.size()), kPosClrDesc}; + EXPECT_FALSE(reader.next().has_value()); + EXPECT_TRUE(reader.failed()); +} + +TEST(GXDlReader, StrideOnlyWalksAndSizes) { + std::vector dl; + draw_cmd(dl, op(GX_TRIANGLESTRIP, GX_VTXFMT0), 3, {0, 0, 1, 1, 2, 2}); + // BP write passes through + dl.insert(dl.end(), {0x61, 0x41, 0x00, 0x00, 0x01}); + draw_cmd(dl, op(GX_TRIANGLES, GX_VTXFMT0), 3, {0, 0, 1, 1, 2, 2}); + + Reader reader{dl.data(), static_cast(dl.size()), static_cast(2)}; + u32 vtxTotal = 0; + u32 passthrough = 0; + while (const auto cmd = reader.next()) { + if (cmd->kind == Command::Kind::Draw) { + vtxTotal += cmd->draw.vtxCount; + } else { + ++passthrough; + } + } + EXPECT_FALSE(reader.failed()); + EXPECT_EQ(vtxTotal, 6u); + EXPECT_EQ(passthrough, 1u); +} + +TEST(GXDlExpand, StripFanQuadWinding) { + using Tri = std::array; + EXPECT_EQ(collect_triangles(GX_TRIANGLESTRIP, 5), (std::vector{{0, 1, 2}, {2, 1, 3}, {2, 3, 4}})); + EXPECT_EQ(collect_triangles(GX_TRIANGLEFAN, 4), (std::vector{{0, 1, 2}, {0, 2, 3}})); + EXPECT_EQ(collect_triangles(GX_QUADS, 8), (std::vector{{0, 1, 2}, {2, 3, 0}, {4, 5, 6}, {6, 7, 4}})); + EXPECT_EQ(collect_triangles(GX_TRIANGLES, 3), (std::vector{{0, 1, 2}})); + + EXPECT_FALSE(expand_triangles(GX_LINES, 4, [](u16, u16, u16) {})); + EXPECT_FALSE(expand_triangles(GX_TRIANGLESTRIP, 2, [](u16, u16, u16) {})); + EXPECT_FALSE(expand_triangles(GX_QUADS, 6, [](u16, u16, u16) {})); +} + +TEST(GXDlOptimize, MergesAdjacentStrips) { + std::vector dl; + draw_cmd(dl, op(GX_TRIANGLESTRIP, GX_VTXFMT0), 4, {0, 0, 1, 1, 2, 2, 3, 3}); + draw_cmd(dl, op(GX_TRIANGLESTRIP, GX_VTXFMT0), 4, {4, 4, 5, 5, 6, 6, 7, 7}); + dl.push_back(GX_NOP); + + const auto result = optimize(dl.data(), static_cast(dl.size()), kPosClrDesc); + ASSERT_TRUE(result.has_value()); + const auto& out = *result; + + // One DRAW_INDEXED command: 10-byte header, 12 u16 indices, 8 2-byte vertices + ASSERT_EQ(out.size(), 10u + 12 * 2 + 8 * 2); + EXPECT_EQ(out[0], GX_AURORA); + EXPECT_EQ((out[1] << 8 | out[2]), GX_AURORA_DRAW_INDEXED); + EXPECT_EQ(out[3], op(GX_TRIANGLES, GX_VTXFMT0)); + EXPECT_EQ((out[4] << 8 | out[5]), 8); // vtxCount + EXPECT_EQ((out[6] << 24 | out[7] << 16 | out[8] << 8 | out[9]), 12); // indexCount + + // Host-endian indices: strip 0 at base 0, strip 1 at base 4 + const u16 expected[12] = {0, 1, 2, 2, 1, 3, 4, 5, 6, 6, 5, 7}; + for (int i = 0; i < 12; i++) { + EXPECT_EQ(host_u16(out.data() + 10 + i * 2), expected[i]) << "index " << i; + } + + // Vertex tuples concatenated verbatim + const u8 expectedVerts[16] = {0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7}; + EXPECT_EQ(std::memcmp(out.data() + 10 + 12 * 2, expectedVerts, sizeof(expectedVerts)), 0); +} + +TEST(GXDlOptimize, PureTrianglesStayPlain) { + std::vector dl; + draw_cmd(dl, op(GX_TRIANGLES, GX_VTXFMT0), 3, {0, 0, 1, 1, 2, 2}); + draw_cmd(dl, op(GX_TRIANGLES, GX_VTXFMT0), 3, {3, 3, 4, 4, 5, 5}); + + const auto result = optimize(dl.data(), static_cast(dl.size()), kPosClrDesc); + ASSERT_TRUE(result.has_value()); + const auto& out = *result; + + // Merged into a single plain triangles draw (no index buffer needed at runtime) + ASSERT_EQ(out.size(), 3u + 6 * 2); + EXPECT_EQ(out[0], op(GX_TRIANGLES, GX_VTXFMT0)); + EXPECT_EQ((out[1] << 8 | out[2]), 6); +} + +TEST(GXDlOptimize, StateCommandIsBarrier) { + std::vector dl; + draw_cmd(dl, op(GX_TRIANGLESTRIP, GX_VTXFMT0), 3, {0, 0, 1, 1, 2, 2}); + const u8 bpCmd[] = {0x61, 0x41, 0x00, 0x00, 0x01}; + dl.insert(dl.end(), std::begin(bpCmd), std::end(bpCmd)); + draw_cmd(dl, op(GX_TRIANGLESTRIP, GX_VTXFMT0), 3, {3, 3, 4, 4, 5, 5}); + + const auto result = optimize(dl.data(), static_cast(dl.size()), kPosClrDesc); + ASSERT_TRUE(result.has_value()); + const auto& out = *result; + + // DRAW_INDEXED(3 verts, 3 indices), BP, DRAW_INDEXED(3 verts, 3 indices) + const u32 drawSize = 10 + 3 * 2 + 3 * 2; + ASSERT_EQ(out.size(), drawSize * 2 + sizeof(bpCmd)); + EXPECT_EQ(out[0], GX_AURORA); + EXPECT_EQ(std::memcmp(out.data() + drawSize, bpCmd, sizeof(bpCmd)), 0); + EXPECT_EQ(out[drawSize + sizeof(bpCmd)], GX_AURORA); + + // Re-walking the optimized list yields DrawIndexed commands with the same vertices + Reader reader{out.data(), static_cast(out.size()), kPosClrDesc}; + auto cmd = reader.next(); + ASSERT_TRUE(cmd.has_value()); + ASSERT_EQ(cmd->kind, Command::Kind::DrawIndexed); + EXPECT_EQ(cmd->draw.vtxCount, 3); + EXPECT_EQ(cmd->draw.indexCount, 3u); + EXPECT_EQ(cmd->draw.index(2), 2); + EXPECT_EQ(cmd->draw.attr_idx(1, GX_VA_POS), 1); + cmd = reader.next(); + ASSERT_TRUE(cmd.has_value()); + EXPECT_EQ(cmd->kind, Command::Kind::Passthrough); + cmd = reader.next(); + ASSERT_TRUE(cmd.has_value()); + ASSERT_EQ(cmd->kind, Command::Kind::DrawIndexed); + EXPECT_EQ(cmd->draw.attr_idx(0, GX_VA_POS), 3); + EXPECT_FALSE(reader.failed()); +} + +TEST(GXDlOptimize, FailsOnDirectAttrWithoutFmt) { + const GXVtxDescList desc[] = { + {GX_VA_POS, GX_DIRECT}, + {GX_VA_NULL, GX_NONE}, + }; + std::vector dl; + draw_cmd(dl, op(GX_TRIANGLES, GX_VTXFMT0), 3, {0, 0, 0, 0, 0, 0}); + EXPECT_FALSE(optimize(dl.data(), static_cast(dl.size()), desc).has_value()); +} + +TEST(GXDlOptimize, DirectAttrWithFmt) { + const GXVtxDescList desc[] = { + {GX_VA_PNMTXIDX, GX_DIRECT}, + {GX_VA_POS, GX_DIRECT}, + {GX_VA_NULL, GX_NONE}, + }; + const GXVtxAttrFmtList fmt0[] = { + {GX_VA_POS, GX_POS_XYZ, GX_S16, 0}, + {GX_VA_NULL, GX_POS_XYZ, GX_U8, 0}, + }; + const VtxFmtLists fmts{fmt0}; + + // Stride: 1 (pnmtxidx) + 6 (3x s16) = 7; quad of 4 vertices + std::vector dl; + dl.push_back(op(GX_QUADS, GX_VTXFMT0)); + be16(dl, 4); + for (u8 v = 0; v < 4; v++) { + dl.push_back(v * 3); // pnmtxidx + for (int b = 0; b < 6; b++) { + dl.push_back(v); + } + } + + const auto result = optimize(dl.data(), static_cast(dl.size()), desc, &fmts); + ASSERT_TRUE(result.has_value()); + // DRAW_INDEXED: 10-byte header, 6 u16 indices, 4 7-byte vertices + ASSERT_EQ(result->size(), 10u + 6 * 2 + 4 * 7); + + Reader reader{result->data(), static_cast(result->size()), desc, &fmts}; + const auto cmd = reader.next(); + ASSERT_TRUE(cmd.has_value()); + ASSERT_EQ(cmd->kind, Command::Kind::DrawIndexed); + EXPECT_EQ(cmd->draw.layout->stride, 7); + EXPECT_EQ(cmd->draw.attr_idx(2, GX_VA_PNMTXIDX), 6); +} + +TEST_F(GXFifoTest, DrawIndexed_RoundTripThroughProcessor) { + std::vector dl; + draw_cmd(dl, op(GX_TRIANGLESTRIP, GX_VTXFMT0), 4, {0, 0, 1, 1, 2, 2, 3, 3}); + draw_cmd(dl, op(GX_TRIANGLEFAN, GX_VTXFMT0), 4, {4, 4, 5, 5, 6, 6, 7, 7}); + + const auto result = optimize(dl.data(), static_cast(dl.size()), kPosClrDesc); + ASSERT_TRUE(result.has_value()); + + // Match the optimizer's descriptor in runtime CP state + gxState().vtxDesc[GX_VA_POS] = GX_INDEX8; + gxState().vtxDesc[GX_VA_CLR0] = GX_INDEX8; + + aurora::gfx::g_testDrawCount = 0; + decode_fifo(*result); + + EXPECT_EQ(aurora::gfx::g_testDrawCount, 1u); + EXPECT_EQ(aurora::gfx::g_testLastDraw.vtxCount, 8u); + EXPECT_EQ(aurora::gfx::g_testLastDraw.indexCount, 12u); + EXPECT_EQ(aurora::gfx::g_testLastDraw.instanceCount, 1u); +} diff --git a/tests/gx_fifo_test.cpp b/tests/gx_fifo_test.cpp index 0167e11..f42352d 100644 --- a/tests/gx_fifo_test.cpp +++ b/tests/gx_fifo_test.cpp @@ -17,7 +17,7 @@ static bool has_bp_write(const std::vector& bytes, u8 reg) { } static bool has_aurora_cmd(const std::vector& bytes, u16 cmd) { - const std::array pattern{GX_LOAD_AURORA, static_cast(cmd >> 8), static_cast(cmd & 0xFF)}; + const std::array pattern{GX_AURORA, static_cast(cmd >> 8), static_cast(cmd & 0xFF)}; return std::search(bytes.begin(), bytes.end(), pattern.begin(), pattern.end()) != bytes.end(); } @@ -1139,9 +1139,9 @@ TEST_F(GXFifoTest, SetArray_Pos_EncodesAuroraArrayBaseAndStride) { auto bytes = capture_fifo(); ASSERT_EQ(bytes.size(), 22u); - EXPECT_EQ(bytes[0], GX_LOAD_AURORA); + EXPECT_EQ(bytes[0], GX_AURORA); EXPECT_EQ(bytes[1], 0x00); - EXPECT_EQ(bytes[2], GX_LOAD_AURORA_ARRAYBASE); + EXPECT_EQ(bytes[2], GX_AURORA_LOAD_ARRAYBASE); const auto expect_be64 = [&](size_t offset, u64 value) { for (size_t i = 0; i < 8; ++i) { @@ -1187,9 +1187,9 @@ TEST_F(GXFifoTest, SetArray_Nbt_UsesNrmCommandSlotAndState) { auto bytes = capture_fifo(); ASSERT_EQ(bytes.size(), 22u); - EXPECT_EQ(bytes[0], GX_LOAD_AURORA); + EXPECT_EQ(bytes[0], GX_AURORA); EXPECT_EQ(bytes[1], 0x00); - EXPECT_EQ(bytes[2], GX_LOAD_AURORA_ARRAYBASE | 0x01); + EXPECT_EQ(bytes[2], GX_AURORA_LOAD_ARRAYBASE | 0x01); EXPECT_EQ(bytes[15], 0); EXPECT_EQ(bytes[16], GX_LOAD_CP_REG); EXPECT_EQ(bytes[17], GX_CP_REG_ARRAYSTRIDE | 0x01); @@ -1223,9 +1223,9 @@ TEST_F(GXFifoTest, SetArray_LittleEndianFlag_UpdatesStateAndClearsCachedRange) { auto bytes = capture_fifo(); ASSERT_EQ(bytes.size(), 22u); - EXPECT_EQ(bytes[0], GX_LOAD_AURORA); + EXPECT_EQ(bytes[0], GX_AURORA); EXPECT_EQ(bytes[1], 0x00); - EXPECT_EQ(bytes[2], GX_LOAD_AURORA_ARRAYBASE | (GX_VA_CLR0 - GX_VA_POS)); + EXPECT_EQ(bytes[2], GX_AURORA_LOAD_ARRAYBASE | (GX_VA_CLR0 - GX_VA_POS)); EXPECT_EQ(bytes[15], 1); EXPECT_EQ(bytes[16], GX_LOAD_CP_REG); EXPECT_EQ(bytes[17], GX_CP_REG_ARRAYSTRIDE | (GX_VA_CLR0 - GX_VA_POS)); @@ -1263,7 +1263,7 @@ TEST_F(GXFifoTest, LoadTexObj_EncodesSdkBpBurstAndAuroraMetadata) { EXPECT_TRUE(has_bp_write(bytes, 0x8E)); EXPECT_TRUE(has_bp_write(bytes, 0x92)); EXPECT_TRUE(has_bp_write(bytes, 0x96)); - EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_TEXOBJ)); + EXPECT_TRUE(has_aurora_cmd(bytes, GX_AURORA_LOAD_TEXOBJ)); reset_gx_state(); decode_fifo(bytes); @@ -1292,7 +1292,7 @@ TEST_F(GXFifoTest, LoadTexObjPcFormat_PreservesFullFormatMetadata) { GXLoadTexObj(&obj, GX_TEXMAP3); auto bytes = capture_fifo(); - EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_TEXOBJ)); + EXPECT_TRUE(has_aurora_cmd(bytes, GX_AURORA_LOAD_TEXOBJ)); reset_gx_state(); decode_fifo(bytes); @@ -1362,8 +1362,8 @@ TEST_F(GXFifoTest, LoadTexObjCiAndTlut_PopulatesTextureAndTlutSlots) { EXPECT_TRUE(has_bp_write(bytes, 0x91)); EXPECT_TRUE(has_bp_write(bytes, 0x95)); EXPECT_TRUE(has_bp_write(bytes, 0x99)); - EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_TEXOBJ)); - EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_TLUT)); + EXPECT_TRUE(has_aurora_cmd(bytes, GX_AURORA_LOAD_TEXOBJ)); + EXPECT_TRUE(has_aurora_cmd(bytes, GX_AURORA_LOAD_TLUT)); reset_gx_state(); decode_fifo(bytes); @@ -1390,7 +1390,7 @@ TEST_F(GXFifoTest, DestroyTexObj_EmitsAuroraDestroyCommandAndClearsIdentity) { GXDestroyTexObj(&obj); auto bytes = capture_fifo(); - EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_DESTROY_TEXOBJ)); + EXPECT_TRUE(has_aurora_cmd(bytes, GX_AURORA_DESTROY_TEXOBJ)); EXPECT_EQ(reinterpret_cast(&obj)->texObjId, 0u); reset_gx_state(); @@ -1438,7 +1438,7 @@ TEST_F(GXFifoTest, DestroyTlutObj_EmitsAuroraDestroyCommandAndClearsIdentity) { GXDestroyTlutObj(&obj); auto bytes = capture_fifo(); - EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_DESTROY_TLUT)); + EXPECT_TRUE(has_aurora_cmd(bytes, GX_AURORA_DESTROY_TLUT)); EXPECT_EQ(reinterpret_cast(&obj)->tlutObjId, 0u); reset_gx_state(); @@ -1485,7 +1485,7 @@ TEST_F(GXFifoTest, DestroyCopyTex_EmitsAuroraDestroyCommand) { GXDestroyCopyTex(image); auto bytes = capture_fifo(); - EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_DESTROY_COPY_TEX)); + EXPECT_TRUE(has_aurora_cmd(bytes, GX_AURORA_DESTROY_COPY_TEX)); reset_gx_state(); decode_fifo(bytes); @@ -2167,7 +2167,7 @@ TEST_F(GXFifoTest, ViewportRender_EncodesAuroraOverride) { GXSetViewportRender(100.0f, 50.0f, 1280.0f, 720.0f, 0.0f, 1.0f); auto bytes = capture_fifo(); - EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_VIEWPORT_RENDER)); + EXPECT_TRUE(has_aurora_cmd(bytes, GX_AURORA_LOAD_VIEWPORT_RENDER)); reset_gx_state(); decode_fifo(bytes); @@ -2182,7 +2182,7 @@ TEST_F(GXFifoTest, ScissorRender_EncodesAuroraOverride) { GXSetScissorRender(100, 40, 800, 600); auto bytes = capture_fifo(); - EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_SCISSOR_RENDER)); + EXPECT_TRUE(has_aurora_cmd(bytes, GX_AURORA_LOAD_SCISSOR_RENDER)); reset_gx_state(); decode_fifo(bytes); diff --git a/tests/gx_test_stubs.cpp b/tests/gx_test_stubs.cpp index 9908075..9ffde8f 100644 --- a/tests/gx_test_stubs.cpp +++ b/tests/gx_test_stubs.cpp @@ -115,8 +115,6 @@ gfx::Range build_uniform(const ShaderInfo& info, uint32_t vtxStart, const BindGr } void resolve_sampled_textures(const ShaderInfo& info) noexcept {} u8 color_channel(GXChannelID id) noexcept { return 0; } -u8 comp_type_size(GXAttr attr, GXCompType type) noexcept { return 0; } -u8 comp_cnt_count(GXAttr attr, GXCompCnt cnt) noexcept { return 0; } } // namespace aurora::gx // --- Buffer push stubs --- @@ -145,9 +143,13 @@ template <> PipelineRef pipeline_ref(const gx::PipelineConfig& config) { return 0; } +gx::DrawData g_testLastDraw{}; +uint32_t g_testDrawCount = 0; + template <> void push_draw_command(gx::DrawData data) { - // No-op + g_testLastDraw = data; + ++g_testDrawCount; } template <> gx::DrawData* get_last_draw_command() {