From 18995f3d7c87d9534785fc9a09917c6aae8c9c65 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Mon, 13 Apr 2026 13:27:21 +0200 Subject: [PATCH 1/2] Enable some draw call merging in text rendering Don't set state between characters if possible. Next step is page merging so it can do full lines of text at once. --- include/global.h | 2 + .../include/JSystem/JUtility/JUTFont.h | 24 ++++++- .../include/JSystem/JUtility/JUTResFont.h | 11 +++- libs/JSystem/src/J2DGraph/J2DPrint.cpp | 11 +++- libs/JSystem/src/JUtility/JUTResFont.cpp | 66 +++++++++++++------ 5 files changed, 89 insertions(+), 25 deletions(-) diff --git a/include/global.h b/include/global.h index d634332797..eda2a610ec 100644 --- a/include/global.h +++ b/include/global.h @@ -220,10 +220,12 @@ using std::isnan; // Some basic macros that are more convenient than putting down #if blocks for one-line changes. #if TARGET_PC #define IF_DUSK(statement) statement +#define IF_DUSK_ARG(expr) , expr #define IF_NOT_DUSK(statement) #define DUSK_IF_ELSE(dusk, orig) dusk #else #define IF_DUSK(statement) +#define IF_DUSK_ARG(expr) #define IF_NOT_DUSK(statement) statement #define DUSK_IF_ELSE(dusk, orig) orig #endif diff --git a/libs/JSystem/include/JSystem/JUtility/JUTFont.h b/libs/JSystem/include/JSystem/JUtility/JUTFont.h index 2718132048..d07e09a590 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTFont.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTFont.h @@ -5,6 +5,18 @@ #include #include "dusk/endian.h" +#if TARGET_PC +struct FontDrawContext { + int loadedBlock = -1; + int loadedPage = -1; +}; +#define FONT_DRAW_CTX , FontDrawContext* context +#define FONT_DRAW_CTX_ARG , context +#else +#define FONT_DRAW_CTX +#define FONT_DRAW_CTX_ARG +#endif + /** * @ingroup jsystem-jutility * @@ -84,7 +96,12 @@ public: /* 0x0C */ virtual void setGX() = 0; /* 0x10 */ virtual void setGX(JUtility::TColor col1, JUtility::TColor col2) { setGX(); } - /* 0x14 */ virtual f32 drawChar_scale(f32 a1, f32 a2, f32 a3, f32 a4, int a5, bool a6) = 0; + /* 0x14 */ virtual f32 drawChar_scale(f32 a1, f32 a2, f32 a3, f32 a4, int a5, bool a6 FONT_DRAW_CTX) = 0; +#if TARGET_PC + f32 drawChar_scale(f32 a1, f32 a2, f32 a3, f32 a4, int a5, bool a6) { + return drawChar_scale(a1, a2, a3, a4, a5, a6, nullptr); + } +#endif /* 0x18 */ virtual int getLeading() const = 0; /* 0x1C */ virtual s32 getAscent() const = 0; /* 0x20 */ virtual s32 getDescent() const = 0; @@ -97,6 +114,11 @@ public: /* 0x3C */ virtual ResFONT* getResFont() const = 0; /* 0x40 */ virtual bool isLeadByte(int a1) const = 0; +#if TARGET_PC + virtual void pushDrawState() = 0; + virtual void popDrawState() = 0; +#endif + static bool isLeadByte_1Byte(int b) { return false; } diff --git a/libs/JSystem/include/JSystem/JUtility/JUTResFont.h b/libs/JSystem/include/JSystem/JUtility/JUTResFont.h index 8709822d7b..68fd798ba5 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTResFont.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTResFont.h @@ -31,7 +31,7 @@ public: virtual ~JUTResFont(); virtual void setGX(); virtual void setGX(JUtility::TColor, JUtility::TColor); - virtual f32 drawChar_scale(f32, f32, f32, f32, int, bool); + virtual f32 drawChar_scale(f32, f32, f32, f32, int, bool FONT_DRAW_CTX); virtual int getLeading() const; virtual s32 getAscent() const; virtual s32 getDescent() const; @@ -43,7 +43,7 @@ public: virtual int getFontType() const; virtual ResFONT* getResFont() const; virtual bool isLeadByte(int) const; - virtual void loadImage(int, GXTexMapID); + virtual void loadImage(int, GXTexMapID FONT_DRAW_CTX); virtual void setBlock(); JUTResFont(ResFONT const*, JKRHeap*); @@ -53,10 +53,15 @@ public: bool initiate(ResFONT const*, JKRHeap*); bool protected_initiate(ResFONT const*, JKRHeap*); void countBlock(); - void loadFont(int, GXTexMapID, JUTFont::TWidth*); + void loadFont(int, GXTexMapID, JUTFont::TWidth* FONT_DRAW_CTX); int getFontCode(int) const; int convertSjis(int, BE(u16)*) const; +#if TARGET_PC + void pushDrawState() override; + void popDrawState() override; +#endif + inline void delete_and_initialize() { deleteMemBlocks_ResFont(); initialize_state(); diff --git a/libs/JSystem/src/J2DGraph/J2DPrint.cpp b/libs/JSystem/src/J2DGraph/J2DPrint.cpp index bd7156f706..69d71aa6c5 100644 --- a/libs/JSystem/src/J2DGraph/J2DPrint.cpp +++ b/libs/JSystem/src/J2DGraph/J2DPrint.cpp @@ -211,6 +211,11 @@ f32 J2DPrint::parse(const u8* pString, int length, int param_2, u16* param_3, local_bc.a = local_bc.a * alpha / 0xFF; mFont->setGradColor(local_b8, field_0x22 ? local_bc : local_b8); +#if TARGET_PC + FontDrawContext context; + mFont->pushDrawState(); +#endif + do { bool b2ByteCharacter = false; bool r25; @@ -312,9 +317,9 @@ f32 J2DPrint::parse(const u8* pString, int length, int param_2, u16* param_3, } else { if (param_6) { if (param_3 != NULL) { - mFont->drawChar_scale(mCursorH + (s16)param_3[someIndex], mCursorV, (s32)mScaleX, (s32)mScaleY, iCharacter, true); + mFont->drawChar_scale(mCursorH + (s16)param_3[someIndex], mCursorV, (s32)mScaleX, (s32)mScaleY, iCharacter, true IF_DUSK_ARG(&context)); } else { - mFont->drawChar_scale(mCursorH, mCursorV, (s32)mScaleX, (s32)mScaleY, iCharacter, true); + mFont->drawChar_scale(mCursorH, mCursorV, (s32)mScaleX, (s32)mScaleY, iCharacter, true IF_DUSK_ARG(&context)); } } mCursorH += field_0x34; @@ -353,6 +358,8 @@ f32 J2DPrint::parse(const u8* pString, int length, int param_2, u16* param_3, iCharacter = *(pString++); } while (true); + IF_DUSK(mFont->popDrawState()); + if (param_3 != NULL) { param_3[someIndex] = 0xFFFF; } diff --git a/libs/JSystem/src/JUtility/JUTResFont.cpp b/libs/JSystem/src/JUtility/JUTResFont.cpp index 206968f053..8583d42c58 100644 --- a/libs/JSystem/src/JUtility/JUTResFont.cpp +++ b/libs/JSystem/src/JUtility/JUTResFont.cpp @@ -231,14 +231,14 @@ void JUTResFont::setGX(JUtility::TColor col1, JUtility::TColor col2) { } f32 JUTResFont::drawChar_scale(f32 pos_x, f32 pos_y, f32 scale_x, f32 scale_y, int str_int, - bool flag) { + bool flag FONT_DRAW_CTX) { f32 x1; f32 x2; f32 y1; JUT_ASSERT(378, mValid); JUTFont::TWidth width; - loadFont(str_int, GX_TEXMAP0, &width); + loadFont(str_int, GX_TEXMAP0, &width FONT_DRAW_CTX_ARG); if ((mFixed) || (!flag)) { x1 = pos_x; @@ -266,7 +266,13 @@ f32 JUTResFont::drawChar_scale(f32 pos_x, f32 pos_y, f32 scale_x, f32 scale_y, i s32 u2 = ((mWidth + cellW) * 0x8000) / texW; s32 v2 = ((mHeight + cellH) * 0x8000) / texH; +#if TARGET_PC + if (!context) { + pushDrawState(); + } +#else GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); +#endif GXBegin(GX_QUADS, GX_VTXFMT0, 4); // Bottom Left @@ -290,18 +296,33 @@ f32 JUTResFont::drawChar_scale(f32 pos_x, f32 pos_y, f32 scale_x, f32 scale_y, i GXTexCoord2u16(u1, v2); GXEnd(); +#if TARGET_PC + if (!context) { + popDrawState(); + } +#else GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_S16, 0); - +#endif return retval; } -void JUTResFont::loadFont(int code, GXTexMapID texMapID, JUTFont::TWidth* pDstWidth) { +#if TARGET_PC +void JUTResFont::pushDrawState() { + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); +} + +void JUTResFont::popDrawState() { + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_S16, 0); +} +#endif + +void JUTResFont::loadFont(int code, GXTexMapID texMapID, JUTFont::TWidth* pDstWidth FONT_DRAW_CTX) { if (pDstWidth != 0) { getWidthEntry(code, pDstWidth); } int fontCode = getFontCode(code); - loadImage(fontCode, texMapID); + loadImage(fontCode, texMapID FONT_DRAW_CTX_ARG); } void JUTResFont::getWidthEntry(int code, JUTFont::TWidth* i_width) const { @@ -403,7 +424,7 @@ int JUTResFont::getFontCode(int chr) const { return ret; } -void JUTResFont::loadImage(int code, GXTexMapID id){ +void JUTResFont::loadImage(int code, GXTexMapID id FONT_DRAW_CTX){ int i = 0; for (; i < mGly1BlockNum; i++) { @@ -435,22 +456,29 @@ void JUTResFont::loadImage(int code, GXTexMapID id){ mHeight = cellRow * cellH; #if TARGET_PC - const auto found = mGlyphTextures->textures.find(pageIdx); - GXTexObj* texObj; - if (found == mGlyphTextures->textures.end()) { - texObj = &mGlyphTextures->textures[pageIdx]; - void* pImg = &mpGlyphBlocks[i]->data[pageIdx * mpGlyphBlocks[i]->textureSize]; - GXInitTexObj(texObj, pImg, mpGlyphBlocks[i]->textureWidth, - mpGlyphBlocks[i]->textureHeight, (GXTexFmt)(u16)mpGlyphBlocks[i]->textureFormat, - GX_CLAMP, GX_CLAMP, 0); + if (!context || context->loadedPage != pageIdx || context->loadedBlock != i) { + const auto found = mGlyphTextures->textures.find(pageIdx); + GXTexObj* texObj; + if (found == mGlyphTextures->textures.end()) { + texObj = &mGlyphTextures->textures[pageIdx]; + void* pImg = &mpGlyphBlocks[i]->data[pageIdx * mpGlyphBlocks[i]->textureSize]; + GXInitTexObj(texObj, pImg, mpGlyphBlocks[i]->textureWidth, + mpGlyphBlocks[i]->textureHeight, (GXTexFmt)(u16)mpGlyphBlocks[i]->textureFormat, + GX_CLAMP, GX_CLAMP, 0); - GXInitTexObjLOD(texObj, GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, 0U, 0U, GX_ANISO_1); - } else { - texObj = &found->second; + GXInitTexObjLOD(texObj, GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, 0U, 0U, GX_ANISO_1); + } else { + texObj = &found->second; + } + + GXLoadTexObj(texObj, id); + + if (context) { + context->loadedBlock = i; + context->loadedPage = pageIdx; + } } - GXLoadTexObj(texObj, id); - // Gets used by some other code. mTexPageIdx = pageIdx; field_0x66 = i; From 143aa51eb370aa59941138abd9f26a8c5a7e5a52 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Mon, 13 Apr 2026 19:09:16 +0200 Subject: [PATCH 2/2] Make JUTResFont load all texture data into one texture Together with the previous change, this enables entire blocks of text to be rendered in one draw call. --- .../include/JSystem/JUtility/JUTFont.h | 3 +- .../include/JSystem/JUtility/JUTResFont.h | 14 +++- libs/JSystem/src/JUtility/JUTResFont.cpp | 76 ++++++++++--------- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/libs/JSystem/include/JSystem/JUtility/JUTFont.h b/libs/JSystem/include/JSystem/JUtility/JUTFont.h index d07e09a590..3c1cd0bf1f 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTFont.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTFont.h @@ -7,8 +7,7 @@ #if TARGET_PC struct FontDrawContext { - int loadedBlock = -1; - int loadedPage = -1; + bool isTextureLoaded = false; }; #define FONT_DRAW_CTX , FontDrawContext* context #define FONT_DRAW_CTX_ARG , context diff --git a/libs/JSystem/include/JSystem/JUtility/JUTResFont.h b/libs/JSystem/include/JSystem/JUtility/JUTResFont.h index 68fd798ba5..d9094f72a9 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTResFont.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTResFont.h @@ -18,10 +18,6 @@ struct BlockHeader { BE(u32) size; }; -#if TARGET_PC -struct GlyphTextures; -#endif - /** * @ingroup jsystem-jutility * @@ -91,6 +87,16 @@ public: /* 0x66 */ u16 field_0x66; /* 0x68 */ u16 mMaxCode; /* 0x6C */ const IsLeadByte_func* mIsLeadByte; + +#if TARGET_PC + // Dusk change: we use a single large texture for all characters. + // This enables better draw call merging, ideally enabling entire blocks of + // text to be one draw call. + TGXTexObj mJoinedTextureObject; + u16 mJoinedTextureHeight; + + void initJoinedTexture(); +#endif }; extern u8 const JUTResFONT_Ascfont_fix12[]; diff --git a/libs/JSystem/src/JUtility/JUTResFont.cpp b/libs/JSystem/src/JUtility/JUTResFont.cpp index 8583d42c58..3ef83b227e 100644 --- a/libs/JSystem/src/JUtility/JUTResFont.cpp +++ b/libs/JSystem/src/JUtility/JUTResFont.cpp @@ -6,26 +6,13 @@ #include "JSystem/JUtility/JUTAssert.h" #include "JSystem/JUtility/JUTConsole.h" #include -#include "absl/container/node_hash_map.h" - -#if TARGET_PC -struct GlyphTextures { - absl::node_hash_map textures; -}; -#endif JUTResFont::JUTResFont() { -#if TARGET_PC - mGlyphTextures = new GlyphTextures(); -#endif initialize_state(); JUTFont::initialize_state(); } JUTResFont::JUTResFont(const ResFONT* pFont, JKRHeap* pHeap) { -#if TARGET_PC - mGlyphTextures = new GlyphTextures(); -#endif initialize_state(); JUTFont::initialize_state(); initiate(pFont, pHeap); @@ -33,10 +20,7 @@ JUTResFont::JUTResFont(const ResFONT* pFont, JKRHeap* pHeap) { JUTResFont::~JUTResFont() { #if TARGET_PC - for (auto& pair : mGlyphTextures->textures) { - pair.second.reset(); - } - delete mGlyphTextures; + mJoinedTextureObject.reset(); #endif if (mValid) { @@ -70,6 +54,33 @@ bool JUTResFont::initiate(const ResFONT* pFont, JKRHeap* pHeap) { return true; } +#if TARGET_PC +void JUTResFont::initJoinedTexture() { + if (mGly1BlockNum != 1) { + CRASH("mGly1BlockNum must be 1!"); + } + + const auto& block = *mpGlyphBlocks[0]; + if (block.textureWidth % 8 != 0 || block.textureHeight % 8 != 0) { + // Idk how the GameCube's tiling texture format works so this is a safety check. + CRASH("Texture size not divisible!"); + } + + int pageCount = 0; + u32 pageNumCells = block.numRows * block.numColumns; + for (u32 code = block.startCode; code < block.endCode; code += pageNumCells) { + pageCount += 1; + } + + mJoinedTextureHeight = block.textureHeight * pageCount; + GXInitTexObj(&mJoinedTextureObject, block.data, block.textureWidth, + mJoinedTextureHeight, static_cast(block.textureFormat.host()), + GX_CLAMP, GX_CLAMP, false); + + GXInitTexObjLOD(&mJoinedTextureObject, GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, 0U, 0U, GX_ANISO_1); +} +#endif + bool JUTResFont::protected_initiate(const ResFONT* pFont, JKRHeap* pHeap) { void** p; delete_and_initialize(); @@ -100,8 +111,10 @@ bool JUTResFont::protected_initiate(const ResFONT* pFont, JKRHeap* pHeap) { mpMapBlocks = JKR_NEW_ARRAY_ARGS(ResFONT::MAP1*, mMap1BlockNum, p); } setBlock(); - return true; + IF_DUSK(initJoinedTexture()); + + return true; } void JUTResFont::countBlock() { @@ -258,8 +271,13 @@ f32 JUTResFont::drawChar_scale(f32 pos_x, f32 pos_y, f32 scale_x, f32 scale_y, i f32 y2 = getDescent() * (scale_y / getHeight()) + pos_y; u16 texW = mpGlyphBlocks[field_0x66]->textureWidth; +#if TARGET_PC + u16 texH = mJoinedTextureHeight; +#else u16 texH = mpGlyphBlocks[field_0x66]->textureHeight; +#endif u16 cellW = mpGlyphBlocks[field_0x66]->cellWidth; + u16 cellH = mpGlyphBlocks[field_0x66]->cellHeight; s32 u1 = (mWidth * 0x8000) / texW; s32 v1 = (mHeight * 0x8000) / texH; @@ -456,26 +474,12 @@ void JUTResFont::loadImage(int code, GXTexMapID id FONT_DRAW_CTX){ mHeight = cellRow * cellH; #if TARGET_PC - if (!context || context->loadedPage != pageIdx || context->loadedBlock != i) { - const auto found = mGlyphTextures->textures.find(pageIdx); - GXTexObj* texObj; - if (found == mGlyphTextures->textures.end()) { - texObj = &mGlyphTextures->textures[pageIdx]; - void* pImg = &mpGlyphBlocks[i]->data[pageIdx * mpGlyphBlocks[i]->textureSize]; - GXInitTexObj(texObj, pImg, mpGlyphBlocks[i]->textureWidth, - mpGlyphBlocks[i]->textureHeight, (GXTexFmt)(u16)mpGlyphBlocks[i]->textureFormat, - GX_CLAMP, GX_CLAMP, 0); - - GXInitTexObjLOD(texObj, GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, 0U, 0U, GX_ANISO_1); - } else { - texObj = &found->second; - } - - GXLoadTexObj(texObj, id); + mHeight += texH * pageIdx; + if (!context || !context->isTextureLoaded) { + GXLoadTexObj(&mJoinedTextureObject, id); if (context) { - context->loadedBlock = i; - context->loadedPage = pageIdx; + context->isTextureLoaded = true; } }