Files
dusk/src/display/texture.c
2026-02-05 23:28:26 -06:00

448 lines
12 KiB
C

/**
* 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/palette/palettelist.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;
#if PSP
assertTrue(
width == mathNextPowTwo(width),
"Width must be powers of 2 for PSP"
);
assertTrue(
height == mathNextPowTwo(height),
"Height must be powers of 2 for PSP"
);
#endif
#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.rgba.colors
);
break;
case TEXTURE_FORMAT_ALPHA:
glTexImage2D(
GL_TEXTURE_2D, 0, format, width, height, 0,
format, GL_UNSIGNED_BYTE, (void*)data.alpha.data
);
break;
case TEXTURE_FORMAT_PALETTE:
assertNotNull(data.palette.data, "Palette texture data cannot be NULL");
assertTrue(
data.palette.palette < PALETTE_LIST_COUNT,
"Palette index out of range"
);
// Get and validate the palette.
const palette_t *pal = PALETTE_LIST[data.palette.palette];
assertNotNull(pal, "Palette cannot be NULL");
GLenum err = glGetError();
if(err != GL_NO_ERROR) {
assertUnreachable("GL Error before setting color table");
}
// Do we support paletted textures?
bool_t havePalTex = false;
#if PSP
havePalTex = true;
#else
GLint mask = 0;
glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &mask);
if(mask & GL_CONTEXT_CORE_PROFILE_BIT) {
GLint n=0; glGetIntegerv(GL_NUM_EXTENSIONS, &n);
for(GLint i=0; i<n; ++i) {
const char* e = (const char*)glGetStringi(GL_EXTENSIONS, i);
if(e && strcmp(e, "GL_EXT_paletted_texture")==0) { havePalTex = true; break; }
}
} else {
const char* ext = (const char*)glGetString(GL_EXTENSIONS);
havePalTex = ext && strstr(ext, "GL_EXT_paletted_texture");
}
#endif
if(!havePalTex) {
// Not supported, convert to RGBA using lookup
color_t formatted[width * height];
for(int32_t i = 0; i < width * height; i++) {
uint8_t index = data.palette.data[i];
assertTrue(
index < pal->colorCount,
"Palette index out of range"
);
formatted[i] = pal->colors[index];
}
glTexImage2D(
GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, (void*)formatted
);
} else {
// Supported.
#if PSP
// PSP requires the color table itself to be a power of two
assertTrue(
pal->colorCount == mathNextPowTwo(pal->colorCount),
"Palette color count must be a power of 2 for PSP"
);
#endif
glTexImage2D(
GL_TEXTURE_2D,
0, GL_COLOR_INDEX8_EXT,
width, height,
0, GL_COLOR_INDEX8_EXT,
GL_UNSIGNED_BYTE, (void*)data.palette.data
);
glColorTableEXT(
GL_TEXTURE_2D, GL_RGBA, pal->colorCount, GL_RGBA,
GL_UNSIGNED_BYTE, (const void*)pal->colors
);
}
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_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
texture->ready = true;
#elif DOLPHIN
texture->format = format;
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_CLAMP, GX_CLAMP,
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_CLAMP, GX_CLAMP,
GX_FALSE
);
GX_InitTexObjLOD(
&texture->texObj,
GX_NEAR, GX_NEAR,
0.0f, 0.0f, 0.0f,
GX_FALSE,
GX_FALSE,
GX_ANISO_1
);
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);
#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_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() {
// 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
);
if(!TEXTURE_BOUND) {
GX_SetNumTexGens(0);
GX_SetNumTevStages(1);
GX_SetTevOp(GX_TEVSTAGE0, GX_PASSCLR);
return;
}
// 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_SetTevOrder(
GX_TEVSTAGE0,
GX_TEXCOORD0,
GX_TEXMAP0,
GX_COLOR0A0
);
GX_SetTevOp(GX_TEVSTAGE0, GX_MODULATE);
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