Add aurora::gx::dl reader & optimizer

This commit is contained in:
Luke Street
2026-06-13 10:15:47 -06:00
parent 5143394381
commit 55b993e08e
22 changed files with 1026 additions and 180 deletions
+2
View File
@@ -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
+2 -2
View File
@@ -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
)
+138
View File
@@ -0,0 +1,138 @@
#pragma once
#include <dolphin/gx.h>
#include <array>
#include <optional>
#include <vector>
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<const GXVtxAttrFmtList*, GX_MAX_VTXFMT>;
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<Command> 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<VtxLayout> mLayouts[GX_MAX_VTXFMT];
bool mLayoutComputed[GX_MAX_VTXFMT] = {};
const VtxLayout* layout(GXVtxFmt fmt);
};
template <typename F>
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<u16>(v + 1), static_cast<u16>(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<u16>(v - 2), static_cast<u16>(v - 1), v);
} else {
f(static_cast<u16>(v - 1), static_cast<u16>(v - 2), v);
}
}
return true;
case GX_TRIANGLEFAN:
if (vtxCount < 3) {
return false;
}
for (u16 v = 2; v < vtxCount; ++v) {
f(0, static_cast<u16>(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<u16>(v + 1), static_cast<u16>(v + 2));
f(static_cast<u16>(v + 2), static_cast<u16>(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<std::vector<u8>> optimize(const u8* dl, u32 size, const GXVtxDescList* desc,
const VtxFmtLists* fmts = nullptr);
} // namespace aurora::gx::dl
+1 -1
View File
@@ -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);
}
+23 -14
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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);
}
+1 -1
View File
@@ -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);
+5 -5
View File
@@ -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);
+3 -3
View File
@@ -6,7 +6,7 @@ extern "C" {
void GXDestroyTexObj(GXTexObj* obj_) {
auto* obj = reinterpret_cast<GXTexObj_*>(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<GXTlutObj_*>(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<u64>(dest));
}
}
+1 -1
View File
@@ -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<u64>(data));
GX_WRITE_U32(size);
GX_WRITE_U8(le ? 1 : 0);
+2 -2
View File
@@ -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<u8>(id));
GX_WRITE_U64(reinterpret_cast<u64>(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<u8>(idx));
GX_WRITE_U64(reinterpret_cast<u64>(obj.data));
GX_WRITE_U32(static_cast<u32>(obj.format));
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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)
+110
View File
@@ -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
+64 -26
View File
@@ -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<int32_t>(read_u32(data + pos, bigEndian));
pos += 4;
const int32_t top = static_cast<int32_t>(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<int32_t>(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<const void*>(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<u16>(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<GXVtxFmt>(cmd & CP_VAT_MASK);
const GXPrimitive prim = static_cast<GXPrimitive>(cmd & CP_OPCODE_MASK);
ASSERT(prim == GX_TRIANGLES, "GX_AURORA_DRAW_INDEXED: primitive must be GX_TRIANGLES, got {}",
static_cast<u32>(prim));
const u32 idxBytes = indexCount * static_cast<u32>(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));
}
+357
View File
@@ -0,0 +1,357 @@
#include "aurora/dl.hpp"
#include <dolphin/gx/GXAurora.h>
#include <dolphin/gx/GXCommandList.h>
#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<u16>(data[0]) << 8 | data[1]; }
static u32 read_be32(const u8* data) {
return static_cast<u32>(data[0]) << 24 | static_cast<u32>(data[1]) << 16 | static_cast<u32>(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<VtxLayout> 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<u8>(stride);
attr.size = static_cast<u8>(size);
attr.type = desc->type;
stride += size;
if (stride > 0xFF) {
return std::nullopt;
}
}
if (stride == 0) {
return std::nullopt;
}
out.stride = static_cast<u8>(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<Command> 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<Command> {
Log.warn("Reader: {} (opcode 0x{:02X} at offset {})", what, cmd, start);
mFailed = true;
return std::nullopt;
};
const auto passthrough = [&](const u32 cmdSize) -> std::optional<Command> {
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<GXVtxFmt>(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<GXPrimitive>(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<GXVtxFmt>(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<GXPrimitive>(opcode),
fmt,
vtxCount,
mData + start + 3,
lo,
nullptr,
0,
},
};
}
}
}
namespace {
struct DrawBatch {
GXVtxFmt fmt = GX_VTXFMT0;
u16 vtxCount = 0;
bool allTriangles = true;
std::vector<u8> verts;
std::vector<u16> indices;
};
void push_be16(std::vector<u8>& out, u16 value) {
out.push_back(value >> 8);
out.push_back(value & 0xFF);
}
void push_be32(std::vector<u8>& 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<u8>& out, DrawBatch& batch) {
if (batch.vtxCount != 0) {
if (batch.allTriangles) {
// plain triangle draw does not require an index buffer
out.push_back(static_cast<u8>(GX_TRIANGLES) | static_cast<u8>(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<u8>(GX_TRIANGLES) | static_cast<u8>(batch.fmt));
push_be16(out, batch.vtxCount);
push_be32(out, static_cast<u32>(batch.indices.size()));
// index data is host-endian; see GX_AURORA_DRAW_INDEXED
const auto* idxData = reinterpret_cast<const u8*>(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<std::vector<u8>> optimize(const u8* dl, u32 size, const GXVtxDescList* desc, const VtxFmtLists* fmts) {
Reader reader{dl, size, desc, fmts};
std::vector<u8> 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<u32>(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
-100
View File
@@ -677,106 +677,6 @@ wgpu::RenderPipeline build_pipeline(const PipelineConfig& config, ArrayRef<wgpu:
return g_device.CreateRenderPipeline(&descriptor);
}
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);
}
void populate_pipeline_config(PipelineConfig& config, GXPrimitive primitive, GXVtxFmt fmt) noexcept {
ZoneScoped;
+4
View File
@@ -40,6 +40,10 @@ if (AURORA_ENABLE_GX)
# FIFO/command processor (encoder + decoder)
../lib/gx/fifo.cpp
../lib/gx/command_processor.cpp
# Display list reader/optimizer
../lib/gx/attr_fmt.cpp
../lib/gx/dl.cpp
gx_dl_test.cpp
)
target_include_directories(gx_fifo_tests PRIVATE
+286
View File
@@ -0,0 +1,286 @@
#include "gx_test_common.hpp"
#include "aurora/dl.hpp"
#include "dolphin/gx/GXAurora.h"
#include "gx/pipeline.hpp"
#include <cstring>
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<u8>(prim) | static_cast<u8>(fmt); }
void be16(std::vector<u8>& out, u16 value) {
out.push_back(value >> 8);
out.push_back(value & 0xFF);
}
void draw_cmd(std::vector<u8>& out, u8 opcode, u16 vtxCount, std::initializer_list<u8> 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<std::array<u16, 3>> collect_triangles(GXPrimitive prim, u16 vtxCount) {
std::vector<std::array<u16, 3>> 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<u8> 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<u32>(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<u8> dl{0x70, 0x00, 0x00};
Reader reader{dl.data(), static_cast<u32>(dl.size()), kPosClrDesc};
EXPECT_FALSE(reader.next().has_value());
EXPECT_TRUE(reader.failed());
}
TEST(GXDlReader, FailsOnDrawOverrun) {
std::vector<u8> dl;
draw_cmd(dl, op(GX_TRIANGLESTRIP, GX_VTXFMT0), 100, {0, 0, 1, 1});
Reader reader{dl.data(), static_cast<u32>(dl.size()), kPosClrDesc};
EXPECT_FALSE(reader.next().has_value());
EXPECT_TRUE(reader.failed());
}
TEST(GXDlReader, StrideOnlyWalksAndSizes) {
std::vector<u8> 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<u32>(dl.size()), static_cast<u8>(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<u16, 3>;
EXPECT_EQ(collect_triangles(GX_TRIANGLESTRIP, 5), (std::vector<Tri>{{0, 1, 2}, {2, 1, 3}, {2, 3, 4}}));
EXPECT_EQ(collect_triangles(GX_TRIANGLEFAN, 4), (std::vector<Tri>{{0, 1, 2}, {0, 2, 3}}));
EXPECT_EQ(collect_triangles(GX_QUADS, 8), (std::vector<Tri>{{0, 1, 2}, {2, 3, 0}, {4, 5, 6}, {6, 7, 4}}));
EXPECT_EQ(collect_triangles(GX_TRIANGLES, 3), (std::vector<Tri>{{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<u8> 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<u32>(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<u8> 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<u32>(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<u8> 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<u32>(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<u32>(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<u8> dl;
draw_cmd(dl, op(GX_TRIANGLES, GX_VTXFMT0), 3, {0, 0, 0, 0, 0, 0});
EXPECT_FALSE(optimize(dl.data(), static_cast<u32>(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<u8> 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<u32>(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<u32>(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<u8> 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<u32>(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);
}

Some files were not shown because too many files have changed in this diff Show More