/** * Copyright (c) 2025 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "texture.h" #include "assert/assert.h" #include "util/memory.h" #include "util/math.h" #include "display/display.h" const texture_t *TEXTURE_BOUND = NULL; void textureInit( texture_t *texture, const int32_t width, const int32_t height, const textureformat_t format, const texturedata_t data ) { assertNotNull(texture, "Texture cannot be NULL"); assertTrue(width > 0 && height > 0, "width/height must be greater than 0"); memoryZero(texture, sizeof(texture_t)); texture->width = width; texture->height = height; texture->format = format; assertTrue(width == mathNextPowTwo(width), "Width must be a power of 2."); assertTrue(height == mathNextPowTwo(height), "Height must be a power of 2."); #if DISPLAY_SDL2 glGenTextures(1, &texture->id); glBindTexture(GL_TEXTURE_2D, texture->id); switch(format) { case TEXTURE_FORMAT_RGBA: glTexImage2D( GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, (void*)data.rgbaColors ); break; case TEXTURE_FORMAT_PALETTE: assertNotNull(data.paletteData, "Palette texture data cannot be NULL"); if(DISPLAY.usingShaderedPalettes) { // Palette textures not supported, convert to GL_RED style texture // so shader can perform the lookup. uint8_t formatted[width * height]; for(int32_t i = 0; i < width * height; i++) { uint8_t index = data.paletteData[i]; formatted[i] = index * 128; } glTexImage2D( GL_TEXTURE_2D, 0, GL_R8, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, (void*)formatted ); } else { glTexImage2D( GL_TEXTURE_2D, 0, GL_COLOR_INDEX8_EXT, width, height, 0, GL_COLOR_INDEX8_EXT, GL_UNSIGNED_BYTE, (void*)data.paletteData ); // glColorTableEXT( // GL_TEXTURE_2D, GL_RGBA, data.palette.palette->colorCount, GL_RGBA, // GL_UNSIGNED_BYTE, (const void*)data.palette.palette->colors // ); } GLenum err = glGetError(); if(err != GL_NO_ERROR) { printf("GL Error uploading palette texture: %d\n", err); assertUnreachable("GL error uploading palette texture"); } break; default: assertUnreachable("Unknown texture format"); break; } glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glBindTexture(GL_TEXTURE_2D, 0); texture->ready = true; #elif DOLPHIN switch(format) { case TEXTURE_FORMAT_RGBA: assertTrue( (width % 4) == 0 && (height % 4) == 0, "RGB5A3 requires w/h multiple of 4 (or pad)" ); // Convert to RGB5A3 format size_t rgbaSize = width * height * sizeof(u16); texture->rgba = (u16*)memalign(32, rgbaSize); assertNotNull(texture->rgba, "Failed to allocate texture RGBA data"); for(uint32_t y = 0; y < height; ++y) { for(uint32_t x = 0; x < width; ++x) { const int src = y * width + x; const int tileX = x >> 2; const int tileY = y >> 2; const int tilesPerRow = width >> 2; const int tileIndex = tileY * tilesPerRow + tileX; const int tileBaseWords = tileIndex * 16; const int inTile = ((y & 3) << 2) + (x & 3); const int dest = tileBaseWords + inTile; color4b_t col = data.rgba.colors[src]; u16 outCol; if(col.a < 255) { // 0AAA RRRR GGGG BBBB outCol = ( (0u << 15) | ((u16)(col.a >> 5) << 12) | ((u16)(col.r >> 4) << 8) | ((u16)(col.g >> 4) << 4) | ((u16)(col.b >> 4) << 0) ); } else { // 1RRRR RRGG GGGB BBBB outCol = ( (1u << 15) | ((u16)(col.r >> 3) << 10) | ((u16)(col.g >> 3) << 5) | ((u16)(col.b >> 3) << 0) ); } texture->rgba[dest] = outCol; } } DCFlushRange(texture->rgba, rgbaSize); GX_InitTexObj( &texture->texObj, texture->rgba, width, height, GX_TF_RGB5A3, GX_REPEAT, GX_REPEAT, GX_FALSE ); DCFlushRange(texture->rgba, rgbaSize); GX_InvalidateTexAll(); GX_InitTexObjLOD( &texture->texObj, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1 ); break; case TEXTURE_FORMAT_ALPHA: { assertTrue( (width % 4) == 0 && (height % 4) == 0, "GX_TF_I8 requires w/h multiple of 4 (or pad)" ); // 1 byte per pixel (I8), GX expects 4x4 tiled layout const size_t alphaSize = (size_t)width * (size_t)height; texture->alpha = (u8*)memalign(32, alphaSize); assertNotNull(texture->alpha, "Failed to allocate alpha texture data"); const u32 tilesPerRow = ((u32)width) >> 3; // /8 for (u32 y = 0; y < (u32)height; ++y) { const u32 tileY = y >> 2; // /4 const u32 inTileY = (y & 3) << 3; // (y%4)*8 for (u32 x = 0; x < (u32)width; ++x) { const u32 srcI = y * (u32)width + x; const u8 srcA = data.alpha.data[srcI]; // linear input const u32 tileX = x >> 3; // /8 const u32 tileIndex = tileY * tilesPerRow + tileX; const u32 tileBase = tileIndex * 32; // 8*4*1 = 32 bytes per tile const u32 inTile = inTileY + (x & 7); // (y%4)*8 + (x%8) texture->alpha[tileBase + inTile] = 0xFF - srcA;// Fixes inverted alpha. } } // Flush CPU cache so GX sees the swizzled I8 texture data DCFlushRange(texture->alpha, alphaSize); // Initialize GX texture object with swizzled data GX_InitTexObj( &texture->texObj, texture->alpha, width, height, GX_TF_I8, GX_REPEAT, GX_REPEAT, GX_FALSE ); GX_InitTexObjLOD( &texture->texObj, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1 ); break; } case TEXTURE_FORMAT_PALETTE: { // Not supported, convert to RGBA using lookup color_t* formatted = memoryAllocate(width * height * sizeof(color_t)); for(int32_t i = 0; i < width * height; i++) { uint8_t index = data.palette.data[i]; assertTrue( index < data.palette.palette->colorCount, "Palette index out of range" ); formatted[i] = data.palette.palette->colors[index]; } textureInit( texture, width, height, TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgba = { .colors = formatted } } ); memoryFree(formatted); break; } default: assertUnreachable("Unsupported texture format for Dolphin"); break; } texture->ready = true; #endif } void textureBind(texture_t *texture) { if(TEXTURE_BOUND == texture) return; if(texture == NULL) { #if DISPLAY_SDL2 glDisable(GL_TEXTURE_2D); #elif DOLPHIN GX_SetNumChans(0); #endif TEXTURE_BOUND = NULL; return; } assertTrue(texture->ready, "Texture ID must be ready"); assertTrue( texture->width > 0 && texture->height > 0, "Texture width and height must be greater than 0" ); #if DISPLAY_SDL2 glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, texture->id); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); #elif DOLPHIN GX_SetNumChans(1); GX_LoadTexObj(&texture->texObj, GX_TEXMAP0); #endif TEXTURE_BOUND = texture; } void textureDispose(texture_t *texture) { assertNotNull(texture, "Texture cannot be NULL"); assertTrue(texture->ready, "Texture ID must be ready"); if(TEXTURE_BOUND == texture) { textureBind(NULL); } #if DISPLAY_SDL2 glDeleteTextures(1, &texture->id); #elif DOLPHIN switch(texture->format) { case TEXTURE_FORMAT_RGBA: free(texture->rgba); break; case TEXTURE_FORMAT_ALPHA: free(texture->alpha); break; default: break; } memoryZero(texture, sizeof(texture_t)); #endif } #if DOLPHIN void textureDolphinUploadTEV() { if(TEXTURE_BOUND == NULL) { GX_SetNumTexGens(0); GX_SetNumTevStages(1); GX_SetTevOp(GX_TEVSTAGE0, GX_PASSCLR); return; } // Add channel for vertex color GX_SetNumChans(1); GX_SetChanCtrl( GX_COLOR0A0,// Store in color channel 0 GX_DISABLE,// Lighting disabled GX_SRC_REG,// Ambient color? GX_SRC_VTX,// Material color? GX_LIGHTNULL,// Light Mask GX_DF_NONE,// Diffuse function GX_AF_NONE// Attenuation function ); // One set of UVs GX_SetNumTexGens(1); GX_SetTexCoordGen( GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY ); // Basically the shader setup switch(TEXTURE_BOUND->format) { case TEXTURE_FORMAT_RGBA: // One TEV stage: vertex color * texture color GX_SetNumTevStages(1); GX_SetTevOp(GX_TEVSTAGE0, GX_MODULATE); GX_SetTevOrder( GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0 ); GX_SetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR); GX_SetAlphaCompare(GX_ALWAYS, 0, GX_AOP_AND, GX_ALWAYS, 0); break; case TEXTURE_FORMAT_ALPHA: // One TEV stage: vertex color * texture color GX_SetNumTevStages(1); GX_SetTevOrder( GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0 ); // Color = vertex color GX_SetTevColorIn( GX_TEVSTAGE0, GX_CC_RASC, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO ); GX_SetTevColorOp( GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV ); // Alpha = vertex alpha * I8 intensity GX_SetTevAlphaIn( GX_TEVSTAGE0, GX_CA_RASA, GX_CA_ZERO, GX_CA_TEXA, GX_CA_ZERO ); GX_SetTevAlphaOp( GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV ); GX_SetBlendMode( GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR ); GX_SetColorUpdate(GX_TRUE); GX_SetAlphaUpdate(GX_TRUE); break; default: assertUnreachable("Unknown texture format in meshDraw"); break; } } #endif