nCine::RHI::GS::GsVram class

Page allocator for the Graphics Synthesizer's 4 MB of local memory.

The GS has no memory allocator of its own: a buffer is simply a base address written into a register (TEX0.TBP0 for a texture, FRAME.FBP for a render target), and it is the program's job to make sure two buffers never overlap. This class is that bookkeeping. It hands out pages, and only pages.

Why the granularity is a whole page

Local memory is organised as 512 pages of 8 KB, each page 32 blocks of 256 bytes. Within a page the mapping from a texel coordinate to a block is a swizzle that depends on the pixel storage mode - PSMT8 and PSMCT32 order the blocks of a page differently, and each mode covers a different number of texels per page (see GetPageGeometry()). Two buffers of different modes that share a page therefore interleave unpredictably and corrupt each other, which is a failure mode the Dreamcast's byte-granular pvr_mem_malloc() simply does not have.

Allocating whole pages removes the hazard by construction rather than by care: if no page is ever shared by two buffers, no page can ever mix two storage modes, and the swizzle stays a private matter inside each allocation. The cost is internal fragmentation of up to one page per texture - and for the sheets this engine actually loads it is usually zero, because a power-of-two sheet at least as large as one page in each direction covers an exact number of pages (a 256x256 PSMT8 sheet is exactly 8).

Placement

Every request takes exactly the pages it needs, placed in the shortest free run long enough to hold it (best fit, lowest address first).

It used to round the request up to a power of two and align it to its own size, which buys a buddy allocator's freedom from external fragmentation. That trade is wrong for a cache this small: the rounding costs up to twice the pages a buffer actually needs, and the page counts it was assumed to be free for - power-of-two sheets - are only half the content. The stores that are not power-of-two are exactly the large ones, where the waste is measured in hundreds of kilobytes: a 640x448 PSMCT32 cinematic frame is 140 pages and was rounded to 256, throwing away 928 KB of a 3.3 MB cache, and a level's own mix of sheets lost about a fifth of the window to the same rounding. External fragmentation costs nothing like that here, because it is not fatal: a request that cannot be placed makes the caller evict and retry (see Residency), and best fit keeps the long runs the big atlases need intact by consuming the short ones first.

Residency

This class only places and frees; it does not decide what should be resident. That is the caller's job - GsTexture keeps the same most-recently-used list, per-frame stamp and host-store rebuild the PVR backend uses, and calls AllocatePages() through it. A failed allocation is reported, not fatal: the caller evicts and retries, exactly as PvrTexture::AllocateVram() does.

Public static variables

static std::uint32_t BlockBytes constexpr
Size of one block, the unit of TEX0.TBP0 and TEX0.CBP.
static std::uint32_t PageBytes constexpr
Size of one page, the unit of FRAME.FBP.
static std::uint32_t BlocksPerPage constexpr
Blocks per page.
static std::uint32_t TotalBytes constexpr
Total local memory of the Graphics Synthesizer.
static std::uint32_t TotalPages constexpr
Total pages of local memory.
static std::uint32_t TotalBlocks constexpr
Total blocks of local memory.
static std::int32_t BufferWidthUnit constexpr
Texels of buffer width one unit of TEX0.TBW/FRAME.FBW stands for.
static std::uint32_t ClutSlotBytes constexpr
Bytes of one CLUT slot (256 entries of 32 bits).
static std::uint32_t InvalidPage constexpr
Returned by the allocators when the request could not be placed.
static std::uint32_t InvalidBlock constexpr
Returned by AllocateClut() when the slab is full.

Public static functions

static void GetPageGeometry(GsPsm psm, std::int32_t& width, std::int32_t& height)
Returns the number of texels one page covers horizontally and vertically in psm.
static auto GetPageCount(GsPsm psm, std::int32_t width, std::int32_t height) -> std::uint32_t
Returns the pages a width x height buffer of psm occupies.
static auto GetBufferWidth(GsPsm psm, std::int32_t width) -> std::int32_t
Returns the TBW/FBW value for a buffer width texels wide in psm.
static auto GetPaddedWidth(GsPsm psm, std::int32_t width) -> std::int32_t
Returns the buffer pitch of a width texel wide buffer of psm, in texels.
static auto Initialize(const GsVramLayout& layout) -> bool
Places the static regions of layout and gives the rest to the texture cache.
static auto IsInitialized() -> bool
Returns true once Initialize() has succeeded.
static auto GetDisplayBufferPage(std::int32_t index) -> std::uint32_t
Returns the first page of display buffer index (FRAME.FBP is a page number).
static auto GetDisplayBufferWidth() -> std::int32_t
Returns the FBW of the display buffers.
static auto GetDisplayPsm() -> GsPsm
Returns the pixel storage mode of the display buffers.
static auto AllocatePages(std::uint32_t pageCount) -> std::uint32_t
Allocates exactly pageCount pages from the texture cache.
static void FreePages(std::uint32_t firstPage, std::uint32_t pageCount)
Returns a run obtained from AllocatePages() to the cache.
static auto AllocateReservedPages(std::uint32_t pageCount) -> std::uint32_t
Allocates pages from the render-target reserve.
static void FreeReservedPages(std::uint32_t firstPage, std::uint32_t pageCount)
Returns a run obtained from AllocateReservedPages().
static auto AllocateClut() -> std::uint32_t
Allocates one CLUT slot, returning its block address (the unit of TEX0.CBP).
static void FreeClut(std::uint32_t block)
Returns a CLUT slot obtained from AllocateClut().
static auto GetCacheFirstPage() -> std::uint32_t
Returns the first page of the texture cache.
static auto GetCachePageCount() -> std::uint32_t
Returns the number of pages in the texture cache.
static auto GetUsedPageCount() -> std::uint32_t
Returns the number of texture-cache pages currently allocated.
static auto GetPeakUsedPageCount() -> std::uint32_t
Returns the high-water mark of GetUsedPageCount().
static auto GetLargestFreeRun() -> std::uint32_t
Returns the length of the longest free run in the texture cache.
static auto GetFailedAllocationCount() -> std::uint32_t
Returns how many allocations have failed since Initialize().

Function documentation

static std::uint32_t nCine::RHI::GS::GsVram::GetPageCount(GsPsm psm, std::int32_t width, std::int32_t height)

Returns the pages a width x height buffer of psm occupies.

Both dimensions are rounded up to the page geometry of the storage mode, so the result is the number of whole pages the buffer's addressing can reach - which is what has to be reserved.

static std::int32_t nCine::RHI::GS::GsVram::GetBufferWidth(GsPsm psm, std::int32_t width)

Returns the TBW/FBW value for a buffer width texels wide in psm.

The buffer pitch is padded to the page geometry like GetPageCount() pads it, because the pages behind it were reserved for the padded pitch. Note that a padded pitch costs no texture coordinate compensation the way the PowerVR's power-of-two padding does: TBW is the pitch and TW/TH are the sampled extent, and the two are independent fields.

static std::int32_t nCine::RHI::GS::GsVram::GetPaddedWidth(GsPsm psm, std::int32_t width)

Returns the buffer pitch of a width texel wide buffer of psm, in texels.

The same padded pitch GetBufferWidth() reports, but not divided into TBW units. Both forms are needed and they are easy to confuse: the raw register field wants TBW, while PS2SDK's libdraw takes texbuffer_t::width/framebuffer_t::width in texels and divides by 64 itself. Handing it a TBW value silently samples a 64-times-too-narrow buffer.

static bool nCine::RHI::GS::GsVram::Initialize(const GsVramLayout& layout)

Places the static regions of layout and gives the rest to the texture cache.

Resets every allocation, so it is also how a test starts from a known state. Returns false if the static regions alone do not fit in local memory, in which case nothing is initialised.

static std::uint32_t nCine::RHI::GS::GsVram::AllocatePages(std::uint32_t pageCount)

Allocates exactly pageCount pages from the texture cache.

Returns the first page, or InvalidPage when no run that long is free - the caller is expected to evict and retry rather than treat it as fatal.

static std::uint32_t nCine::RHI::GS::GsVram::AllocateReservedPages(std::uint32_t pageCount)

Allocates pages from the render-target reserve.

Kept apart from the texture cache so that a render target - which has no host copy and so cannot be evicted and rebuilt - can never be crowded out by streaming textures. A target that does not fit here is not fatal either: GsTexture::SetRenderTarget() falls back to the cache, where it is still exempt from eviction. The reserve is what keeps the common case off the streaming window, not a hard ceiling on render targets.

static std::uint32_t nCine::RHI::GS::GsVram::AllocateClut()

Allocates one CLUT slot, returning its block address (the unit of TEX0.CBP).

Every slot holds a 256-entry 32-bit CLUT, so all of them share the one storage mode and may share pages - the slab is the single place this class sub-allocates a page, and it is safe for exactly that reason. Returns InvalidBlock when the slab is full.

static std::uint32_t nCine::RHI::GS::GsVram::GetLargestFreeRun()

Returns the length of the longest free run in the texture cache.

The difference between this and GetCachePageCount() - GetUsedPageCount() is the fragmentation, which is the number to look at when an allocation fails while the cache is not full.

static std::uint32_t nCine::RHI::GS::GsVram::GetFailedAllocationCount()

Returns how many allocations have failed since Initialize().

The Dreamcast work used exactly this counter to tell a working set that fits from one that thrashes (a level went from 2972 failed allocations to none), and it is the first number to look at when bringing a level up here.