Daniel “Agentlien” Kvick, a graphics programmer at 505 Games, spent last year writing texture-conversion code to port a custom PC engine to consoles. He went in expecting a chore and came out with a tour of how much platform-specific machinery is packed into the word “texture.”

His framing is a debugging problem: given a block of memory holding a texture, converting it means computing, for every element in the hierarchy, the address it will occupy after conversion. Each layer has its own ordering rule, and the rules differ per platform. Running example: a 1024x1024 RGBA texture in BC7.

The layers, from the smallest unit outward:

  • BC7 blocks — lossy compression over 4x4 texel chunks of 16 bytes. Each block stores pairs of reference colors (“end points”); each texel stores an index interpolating between them. One byte per texel, 4x compression for RGBA8, and visually hard to separate from uncompressed.
  • Swizzles — texels are not stored row by row but in patterns like Morton/Z-order, a fractal sweep that keeps visually adjacent texels adjacent in memory. Source and target platforms may want different swizzles.
  • Mips — precomputed halved images that let a texel’s resolution match the render target’s, killing shimmer and Moiré. They sit roughly contiguously, but ascending vs descending order varies by platform, so you cannot walk source and destination pointers in lockstep.
  • Tiles — memory is split into square tiles (typically 64KiB) laid out linearly, which also enables streaming only the visible parts of a large texture. A 1024x1024 BC7 mip is 16 tiles of 256x256 texels.
  • The mip tail — small mips waste a whole tile each, so platforms pack all sub-tile mips into as few tiles as possible. For his example the last 8 mips fit in a single 64KiB tile.
  • Non-contiguous tiles — in a DX12-style heap, tiles stream in and out and fragment. Tiles of one texture end up scattered, so tile index must be translated to an address explicitly.

Two debugging tricks are the most transferable part. Compressed memory dumps are unreadable, and any accidental alignment shift makes everything render as noise because end points and indices get reinterpreted. His fix: overwrite each 16-byte block with four 32-bit integers carrying x, y, z, and mip level, then read the block back out of a dump and compare against the coordinates you expected. For targeted probes, write 16 bytes of zeroes — an invalid block that reliably renders black — and use a run of black blocks as visual markers, with raw debug values in between.

The bug that ties it together: computing addresses as “texture base + global offset” works fine for contiguous textures and silently corrupts non-contiguous ones, overwriting other textures’ tiles while leaving your own uninitialized. He shipped that one before finding it.

What makes the piece worth reading is that the subject is not really texture formats. It is how platform conventions get encoded as universal truths by code that happens to work on one platform. Every layer here is a convention, not a law — and the ones you never had to think about are the ones that bite on the port.