Textures
src/textures/texture_manager.hh — loads and manages .TIM textures in VRAM. See the Asset Pipeline Guides for how to author a .TIM from a source PNG.
static constexpr uint16_t texturePageWidth = 64;
static constexpr uint16_t texturePageHeight = 256;
static constexpr uint8_t texturePageColumns = 16;
static constexpr uint8_t MAX_TEXTURES = 32;
struct TimFile {
uint64_t nameHash;
bool isLoaded; // is this slot actually holding a texture?
uint16_t x, y, width, height;
psyqo::Prim::TPageAttr::ColorMode colourMode; // bits per pixel: 4, 8, or 16
bool hasClut;
uint16_t clutX, clutY;
uint16_t clutWidth, clutHeight; // clutHeight is always 1
};
class TextureManager final {
public:
psyqo::Coroutine<> LoadTIM(const eastl::string_view &textureName, uint16_t x, uint16_t y, uint16_t clutX, uint16_t clutY, TimFile **timOut, uint32_t userFlags = 0);
psyqo::PrimPieces::TPageAttr GetTPageAttr(const TimFile *tim);
psyqo::PrimPieces::TPageAttr GetTPageAttr(const TimFile &tim);
psyqo::Rect GetTPageUVForTim(const TimFile &tim);
psyqo::Rect GetTPageUVForTim(const TimFile *tim);
void GetTextureFromName(const eastl::string_view &textureName, TimFile **timOut);
// dump all textures in memory and start fresh. Used when switching to a
// loading screen. Dangerous — doesn't check what's in use, and doesn't
// clear VRAM itself.
void Dump(void);
};
Non-static member of MadnightEngine (g_madnightEngine.m_textureManager). TimFile now stores a hashed nameHash instead of the full name string, same as VagEntry (Sound) and Billboard (Core).
LoadTIMtakes the target VRAM placement (x,y) and CLUT placement (clutX,clutY) explicitly, and the engine doesn't do automatic VRAM packing, so placement has to be planned per-texture (this is exactly what theTEXTUREentry fields in a SCENEBIN manifest carry).GetTPageAttr/GetTPageUVForTimconvert a loadedTimFile's VRAM placement into the texture-page attribute and UV rect a draw primitive needs — used internally byGameObject/Billboardrendering andRenderer::RenderSprite.- Unless
userFlagshas bit 0 set,LoadTIMsets the STP bit (bit 15) on every non-zero CLUT entry, or on every non-zero pixel of a 16-bit TIM, before uploading. Advanced fog needs this to blend textured faces (see Render). It happens whether or not fog is ever enabled, so a game drawing its own semi-transparent prims from engine-loaded textures will see them blend where they would otherwise draw opaque; load those with bit 0 set. Bit 0 is also what marks a texture with holes, and whatRF_QUICK_ADVANCED_FOGobjects need.texture_extraentries in a SCENEBIN manifest pass theirflagshere. - Up to
MAX_TEXTURES(32) textures can be resident at once. Past that,LoadTIMyields a nullTimFile*in*timOut, so check it.
Usage
TimFile *crateTex;
co_await g_madnightEngine.m_textureManager.LoadTIM("crate.tim", /*x*/ 320, /*y*/ 0, /*clutX*/ 0, /*clutY*/ 240, &crateTex);
crateGameObject->SetTexture("crate.tim"); // looks it up by the same name afterwards
To load a texture with bit 0 set, pass 1 as userFlags. Do this for a texture with holes, for one an RF_QUICK_ADVANCED_FOG object uses, or for one your own semi-transparent prims draw from:
TimFile *fenceTex;
co_await g_madnightEngine.m_textureManager.LoadTIM("fence.tim", 384, 0, 0, 241, &fenceTex, /*userFlags*/ 1);
From a SCENEBIN manifest, use a texture_extra line with flags set to 1, as in texture_extra TEXTURES/SMS.TIM 320 0 0 240 1. A plain texture line always loads with flags 0.
Internals
VRAM placement is entirely manual, and getting x/y/clutX/clutY right requires understanding the layout (spelled out in a long comment at the top of texture_manager.cpp):
- The frame buffers occupy VRAM
0-319, 0-479, so the first free texture page starts atx=320. - Pages are
64×256px each, so a texture must fit within 1, 2, or 4 contiguous pages depending on colour depth — 4-bit textures are squeezed to 1/4 width in VRAM, 8-bit to 1/2 width, 16-bit at full width. - You can't mix colour depths within one page.
- CLUTs have known-safe rows at
y=240-255andy=496-511(X must be a multiple of 16), which is why the usage example above places the CLUT aty=240. - All four placement coordinates are used exactly as given, including 0, which is a real VRAM position. Pass
TIM_POSITION_FROM_FILE(0xFFFF, a value VRAM cannot hold) for any of them to take that coordinate from the TIM file instead. In a scene manifest the same thing is spelledauto.
Getting this wrong doesn't crash: textures can silently overlap or clip in VRAM. The SCENEBIN format is the intended way to keep placement authoritative and consistent per-scene rather than hardcoding coordinates per texture.