A file is not a GPU texture#
A PNG on disk is a compressed representation. A shader needs texels in a GPU resource, a descriptor that describes that resource, and a sampler that defines how to read it. AssetStore connects these forms without making the render thread stop to decode an image. The lifecycle is disk file → decoder → CPU pixels → upload storage → GPU texture → shader-visible descriptor → material binding → sampling → retirement. Each arrow has an owner and a completion condition. A stable TextureHandle can exist before the final resource is ready.
Reference excerpt: existing texture and recording context, after a copy command has been recorded. Lifetime completion is tracked separately.
// Destination texture: copied on this direct queue.
D3D12_RESOURCE_BARRIER b{};
b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
b.Transition.pResource = texture;
b.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
b.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
b.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
list->ResourceBarrier(1, &b);Reference excerpt: existing texture and recording context, after a copy command has been recorded. Lifetime completion is tracked separately.
// Same-family copy → fragment sampling; one mip, one layer.
VkImageMemoryBarrier2 b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2};
b.srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
b.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT;
b.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT;
b.dstAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.image = image;
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
VkDependencyInfo d{VK_STRUCTURE_TYPE_DEPENDENCY_INFO};
d.imageMemoryBarrierCount = 1;
d.pImageMemoryBarriers = &b;
vkCmdPipelineBarrier2(commandBuffer, &d);Inspect the transfer#
Follow a texture to the GPU This illustration uses canvas. The explanation below describes the same process.
Choose a stage and mip level. Decoded rows and upload rows can have different sizes. The lab’s RGBA8 rows use four bytes per texel; DX12 upload RowPitch is padded to the required alignment. Placement offsets have a separate alignment requirement. Always use GetCopyableFootprints for the actual resource, rather than deriving a general layout from width alone.
Requests and publication#
AssetStore tracks Unloaded, Decoding, Uploading, Ready, and Failed states. A decode job validates dimensions, format, and total byte count before allocating storage. UploadManager reserves staging bytes and records a copy. PendingGpuAsset stores the destination resource, descriptor reservation, and completion FencePoint. A material whose requested texture is still loading binds the persistent fallback descriptor. After upload completion and the required resource transition, AssetStore publishes a ready asset version. Rendering resolves that version during extraction or a defined descriptor-update boundary; it does not rewrite an in-flight descriptor arbitrarily.
Implementation: row-pitched staging
D3D12_PLACED_SUBRESOURCE_FOOTPRINT layout{};
UINT rows = 0;
UINT64 rowBytes = 0, totalBytes = 0;
device->GetCopyableFootprints(&textureDesc, 0, 1, 0,
&layout, &rows, &rowBytes, &totalBytes);
// mapped points to an upload buffer of at least totalBytes.
for (UINT y = 0; y < rows; ++y) {
std::memcpy(mapped + layout.Offset + y * layout.Footprint.RowPitch,
pixels.data() + y * decodedStride,
static_cast<size_t>(rowBytes));
}This excerpt assumes an uncompressed RGBA image with validated decodedStride. Compressed block formats have block rows, and arrays or mip chains have multiple subresources. The complete texture tutorial includes decode, allocation, mapping, copy commands, barriers, descriptor creation, and ownership.
Color, mips, and compression#
Base-color and many emissive textures are authored as sRGB. Their sampling view decodes them into linear values before lighting. Roughness, metalness, normals, depth, and masks are data; use linear views. Applying sRGB decoding to a normal map changes its direction. Lighting and intermediate HDR values stay linear. Mipmaps are smaller prefiltered images. Sampling chooses a level from screen-space derivatives so distant texels do not flicker. Anisotropic filtering helps surfaces viewed at a steep angle, within a hardware-supported sampler limit. Normal-map mips may need renormalization; roughness filtering can require more care than averaging arbitrary channels. Offline cooking generates and compresses mips into GPU-supported formats. BC formats store blocks instead of independent four-byte pixels and reduce storage and bandwidth at the cost of compression artifacts. The native sample intentionally uses one uncompressed mip so the transfer is easy to inspect; it does not demonstrate production mip generation.
Caching, streaming, and cleanup#
Cache keys include the source identity, import settings, and format/version. Streaming retains low-resolution mips while prioritizing additional detail by visibility and distance. Sparse or reserved resources are an advanced residency strategy, not a requirement for ordinary asset loading. Retire an asset only after CPU references release it and every queue that used its GPU allocations completes. Descriptor slots, staging spans, and resources can have different retirement points. A device removal invalidates the backend’s resources; rebuild from retained asset metadata rather than reusing stale native handles.
Diagnose an asset failure#
Striped textures often indicate a row-pitch error. Black textures may have an unbound descriptor, wrong state, failed decode, or shader binding mismatch. Washed-out color suggests a color-space mismatch. Intermittent corruption suggests descriptor reuse or staging reuse before completion. Compare the debug-layer message with the resource’s state history and its last-use fence. Official reference: Microsoft resource uploads.