Skip to main content

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).

  • LoadTIM takes 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 the TEXTURE entry fields in a SCENEBIN manifest carry).
  • GetTPageAttr/GetTPageUVForTim convert a loaded TimFile's VRAM placement into the texture-page attribute and UV rect a draw primitive needs — used internally by GameObject/Billboard rendering and Renderer::RenderSprite.
  • Unless userFlags has bit 0 set, LoadTIM sets 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 what RF_QUICK_ADVANCED_FOG objects need. texture_extra entries in a SCENEBIN manifest pass their flags here.
  • Up to MAX_TEXTURES (32) textures can be resident at once. Past that, LoadTIM yields a null TimFile* 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 at x=320.
  • Pages are 64×256 px 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-255 and y=496-511 (X must be a multiple of 16), which is why the usage example above places the CLUT at y=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 spelled auto.

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.