DirectX 12 / PRIMARY BACKEND

Load a texture, end to end

Decode an image with WIC, honor upload footprints, copy to a default texture, publish an SRV, and sample it on indexed geometry.

Prerequisites and expected output#

Build the shared project from setup, then read the first-triangle and descriptor chapters. Open Sample::CreateTexture, DecodeImage, CreateGeometry, CreatePipeline, and Render in samples/dx12/main.cpp. The complete shader is Shaders/Scene.hlsl.

text · REFERENCE EXCERPT
cmake --build build --config Debug --target qubic_texture
build\Debug\qubic_texture.exe
build\Debug\qubic_texture.exe "C:\images\color.png"

The first command displays a generated checker on a rotating cube. The second loads your color image using Windows Imaging Component. The face brightness is a fixed teaching tint. The sample has one mip and no model-file importer or physically based lighting system.

1. Decode and validate#

DecodeImage initializes a WIC decoder, reads frame zero, queries dimensions, converts to 32bpp RGBA, and calls CopyPixels into a vector. It rejects zero or unsupported dimensions and checks that the byte count fits WIC’s UINT interface. With no file it generates a known RGBA checker, which helps distinguish decode problems from transfer problems. The source assumes the provided color image should be interpreted as sRGB. It is not a full color-management pipeline. Production import distinguishes display color from numerical data and records the chosen format in asset metadata.

2. Allocate destination and staging#

CreateTexture creates a default-heap 2D resource in COPY_DEST. GetCopyableFootprints returns the layout, row count, meaningful row bytes, and required upload size. A separate upload-heap buffer in GENERIC_READ holds the padded copy source. Map the upload buffer with a no-read range. For each row, copy meaningful bytes from the tightly packed CPU stride into layout.Offset plus row × layout.Footprint.RowPitch. Unmap records the written range. Padding is not image data; copying width × height × 4 bytes into the buffer as one block would ignore padded rows.

INTERACTIVE ILLUSTRATIONSIMULATION / BROWSER

Follow a texture to the GPU This illustration uses canvas. The explanation below describes the same process.

Change a control to inspect the result. Values describe the simulation, not native engine benchmarks. Open full lab ↗

3. Record the copy and transition#

D3D12_TEXTURE_COPY_LOCATION describes the source as a placed footprint and the destination as subresource zero. CopyTextureRegion copies between them. A transition moves the destination from COPY_DEST to PIXEL_SHADER_RESOURCE on the direct queue. The sample closes and executes this initialization list, signals the queue, and waits once. The local staging ComPtr remains alive throughout that wait. Publishing the SRV afterward is easy to reason about. A production UploadManager instead retains a pending record and publishes readiness asynchronously after completion.

4. Bind and sample#

CreateShaderResourceView fills the one shader-visible SRV slot. The root signature has a descriptor table for t0 and a static linear wrap sampler at s0. Render binds srvs, sets the table handle, supplies the world/view/projection root constants at b0, and draws the indexed cube. The cube duplicates vertices per face so each face has its own UV rectangle. Its depth buffer prevents farther fragments from overwriting nearer ones. sRGB texture sampling produces linear color; the sRGB RTV encodes the shader’s output for display.

Complete source, with no hidden transfer wrapper#

Complete sample source: dx12/main.cpp
// Independent teaching source, not verified QubicEngine repository code.
#include <windows.h>
#include <shellapi.h>
#include <wincodec.h>
#include <wrl/client.h>
#include <d3d12.h>
#include <dxgi1_6.h>
#include <DirectXMath.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <cfloat>
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <limits>
#include <stdexcept>
#include <string>
#include <vector>
using Microsoft::WRL::ComPtr;
using namespace DirectX;
namespace {
void Check(HRESULT hr) {
    if (FAILED(hr)) {
        char text[64];
        std::snprintf(text, sizeof(text), "DirectX/Windows failure: 0x%08lX", static_cast<unsigned long>(hr));
        throw std::runtime_error(text);
    }
}
D3D12_HEAP_PROPERTIES Heap(D3D12_HEAP_TYPE type) {
    D3D12_HEAP_PROPERTIES h{};
    h.Type = type; h.CreationNodeMask = 1; h.VisibleNodeMask = 1;
    return h;
}
D3D12_RESOURCE_DESC BufferDesc(UINT64 size) {
    D3D12_RESOURCE_DESC d{};
    d.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; d.Width = size;
    d.Height = 1; d.DepthOrArraySize = 1; d.MipLevels = 1;
    d.SampleDesc.Count = 1; d.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
    return d;
}
void Transition(ID3D12GraphicsCommandList* list, ID3D12Resource* resource,
                D3D12_RESOURCE_STATES before, D3D12_RESOURCE_STATES after) {
    D3D12_RESOURCE_BARRIER b{};
    b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
    b.Transition.pResource = resource;
    b.Transition.StateBefore = before; b.Transition.StateAfter = after;
    b.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
    list->ResourceBarrier(1, &b);
}
std::vector<char> ReadBytes(const std::filesystem::path& file) {
    std::ifstream stream(file, std::ios::binary | std::ios::ate);
    if (!stream) throw std::runtime_error("Shader file missing. Build shaders and keep the shaders folder next to the executable.");
    const auto size = stream.tellg();
    if (size <= 0) throw std::runtime_error("Empty shader file");
    std::vector<char> bytes(static_cast<size_t>(size));
    stream.seekg(0);
    if (!stream.read(bytes.data(), static_cast<std::streamsize>(bytes.size())))
        throw std::runtime_error("Could not read shader file");
    return bytes;
}
struct Image { UINT width = 0, height = 0; std::vector<uint8_t> pixels; };
Image DecodeImage(const std::filesystem::path& file) {
    Image image;
    if (file.empty()) {
        image.width = image.height = 128;
        image.pixels.resize(128 * 128 * 4);
        for (UINT y = 0; y < 128; ++y) for (UINT x = 0; x < 128; ++x) {
            const bool bright = ((x / 16) + (y / 16)) % 2 == 0;
            const size_t offset = (y * 128 + x) * 4;
            image.pixels[offset] = bright ? 160 : 20;
            image.pixels[offset + 1] = bright ? 235 : 60;
            image.pixels[offset + 2] = bright ? 250 : 80;
            image.pixels[offset + 3] = 255;
        }
        return image;
    }
    ComPtr<IWICImagingFactory> factory;
    Check(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory)));
    ComPtr<IWICBitmapDecoder> decoder;
    Check(factory->CreateDecoderFromFilename(file.c_str(), nullptr, GENERIC_READ,
        WICDecodeMetadataCacheOnLoad, &decoder));
    ComPtr<IWICBitmapFrameDecode> frame;
    Check(decoder->GetFrame(0, &frame));
    Check(frame->GetSize(&image.width, &image.height));
    if (!image.width || !image.height || image.width > 16384 || image.height > 16384)
        throw std::runtime_error("Image dimensions outside the sample's supported limits");
    const UINT64 byteCount = UINT64(image.width) * image.height * 4;
    if (byteCount > std::numeric_limits<UINT>::max()) throw std::runtime_error("Image too large for WIC CopyPixels");
    ComPtr<IWICFormatConverter> converter;
    Check(factory->CreateFormatConverter(&converter));
    Check(converter->Initialize(frame.Get(), GUID_WICPixelFormat32bppRGBA,
        WICBitmapDitherTypeNone, nullptr, 0, WICBitmapPaletteTypeCustom));
    image.pixels.resize(static_cast<size_t>(byteCount));
    Check(converter->CopyPixels(nullptr, image.width * 4, static_cast<UINT>(byteCount), image.pixels.data()));
    return image;
}
struct Vertex { XMFLOAT3 position; XMFLOAT3 color; XMFLOAT2 uv; };
struct FrameContext { ComPtr<ID3D12CommandAllocator> allocator; UINT64 completion = 0; };
class Sample {
public:
    HWND window = nullptr;
    UINT width = 1100, height = 720, pendingWidth = 1100, pendingHeight = 720;
    bool minimized = false;
    std::filesystem::path imageFile;
    bool warp = false;
    static constexpr UINT FrameCount = 2;
    ComPtr<IDXGIFactory6> factory;
    ComPtr<ID3D12Device> device;
    ComPtr<ID3D12CommandQueue> queue;
    ComPtr<IDXGISwapChain3> swap;
    ComPtr<ID3D12DescriptorHeap> rtvs, srvs, dsvs;
    std::array<ComPtr<ID3D12Resource>, FrameCount> backBuffers;
    std::array<FrameContext, FrameCount> frames;
    ComPtr<ID3D12GraphicsCommandList> list;
    ComPtr<ID3D12Fence> fence;
    ComPtr<ID3D12RootSignature> root;
    ComPtr<ID3D12PipelineState> pipeline;
    ComPtr<ID3D12Resource> vertexBuffer, indexBuffer, texture, depth;
    D3D12_VERTEX_BUFFER_VIEW vertexView{};
    D3D12_INDEX_BUFFER_VIEW indexView{};
    UINT indexCount = 0, rtvStride = 0;
    UINT64 nextFence = 1;
    HANDLE event = nullptr;
    std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
    ~Sample() {
        if (window && IsWindow(window)) {
            SetWindowLongPtrW(window, GWLP_USERDATA, 0);
            DestroyWindow(window);
        }
        if (event) {
            try { if (queue && fence) Flush(); } catch (...) {}
            CloseHandle(event);
        }
    }
    void Wait(UINT64 value) {
        if (value && fence->GetCompletedValue() < value) {
            Check(fence->SetEventOnCompletion(value, event));
            if (WaitForSingleObject(event, INFINITE) != WAIT_OBJECT_0)
                throw std::runtime_error("Fence event wait failed");
        }
    }
    UINT64 Signal() { UINT64 v = nextFence++; Check(queue->Signal(fence.Get(), v)); return v; }
    void Flush() { Wait(Signal()); }
    D3D12_CPU_DESCRIPTOR_HANDLE Rtv(UINT i) {
        auto h = rtvs->GetCPUDescriptorHandleForHeapStart(); h.ptr += SIZE_T(i) * rtvStride; return h;
    }
    void Init() {
        UINT flags = 0;
#ifdef _DEBUG
        ComPtr<ID3D12Debug> debug;
        if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debug)))) {
            debug->EnableDebugLayer(); flags |= DXGI_CREATE_FACTORY_DEBUG;
        }
#endif
        Check(CreateDXGIFactory2(flags, IID_PPV_ARGS(&factory)));
        ComPtr<IDXGIAdapter1> adapter;
        if (warp) {
            Check(factory->EnumWarpAdapter(IID_PPV_ARGS(&adapter)));
            Check(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device)));
        } else {
            for (UINT i = 0; ; ++i) {
                const HRESULT result = factory->EnumAdapterByGpuPreference(i,
                    DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter));
                if (result == DXGI_ERROR_NOT_FOUND) break;
                Check(result);
                DXGI_ADAPTER_DESC1 d{}; Check(adapter->GetDesc1(&d));
                if (!(d.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) && SUCCEEDED(D3D12CreateDevice(
                    adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device)))) break;
                adapter.Reset();
            }
            if (!device) throw std::runtime_error("No DX12 device. Try --warp for the software teaching path.");
        }
        D3D12_FEATURE_DATA_SHADER_MODEL shaderModel{D3D_SHADER_MODEL_6_0};
        Check(device->CheckFeatureSupport(D3D12_FEATURE_SHADER_MODEL, &shaderModel, sizeof(shaderModel)));
        if (shaderModel.HighestShaderModel < D3D_SHADER_MODEL_6_0)
            throw std::runtime_error("This sample requires Shader Model 6.0.");
        D3D12_COMMAND_QUEUE_DESC q{}; q.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
        Check(device->CreateCommandQueue(&q, IID_PPV_ARGS(&queue)));
        DXGI_SWAP_CHAIN_DESC1 s{}; s.Width = width; s.Height = height; s.BufferCount = FrameCount;
        s.Format = DXGI_FORMAT_R8G8B8A8_UNORM; s.SampleDesc.Count = 1;
        s.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; s.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
        ComPtr<IDXGISwapChain1> base;
        Check(factory->CreateSwapChainForHwnd(queue.Get(), window, &s, nullptr, nullptr, &base));
        Check(base.As(&swap)); Check(factory->MakeWindowAssociation(window, DXGI_MWA_NO_ALT_ENTER));
        D3D12_DESCRIPTOR_HEAP_DESC h{}; h.NumDescriptors = FrameCount; h.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
        Check(device->CreateDescriptorHeap(&h, IID_PPV_ARGS(&rtvs)));
        rtvStride = device->GetDescriptorHandleIncrementSize(h.Type);
        h.NumDescriptors = 1; h.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; h.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
        Check(device->CreateDescriptorHeap(&h, IID_PPV_ARGS(&srvs)));
        h.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; h.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
        Check(device->CreateDescriptorHeap(&h, IID_PPV_ARGS(&dsvs)));
        for (auto& f : frames) Check(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&f.allocator)));
        Check(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, frames[0].allocator.Get(), nullptr, IID_PPV_ARGS(&list)));
        Check(list->Close());
        Check(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)));
        event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
        if (!event) throw std::runtime_error("CreateEvent failed");
        CreateTargets(); CreatePipeline(); CreateGeometry();
#if TEXTURED
        CreateTexture();
#endif
    }
    void CreateTargets() {
        D3D12_RENDER_TARGET_VIEW_DESC r{}; r.Format = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
        r.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
        for (UINT i = 0; i < FrameCount; ++i) {
            Check(swap->GetBuffer(i, IID_PPV_ARGS(&backBuffers[i])));
            device->CreateRenderTargetView(backBuffers[i].Get(), &r, Rtv(i));
        }
#if TEXTURED
        D3D12_RESOURCE_DESC d{}; d.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
        d.Width = width; d.Height = height; d.DepthOrArraySize = 1; d.MipLevels = 1;
        d.Format = DXGI_FORMAT_D32_FLOAT; d.SampleDesc.Count = 1; d.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
        D3D12_CLEAR_VALUE clear{}; clear.Format = DXGI_FORMAT_D32_FLOAT; clear.DepthStencil.Depth = 1.0f;
        auto h = Heap(D3D12_HEAP_TYPE_DEFAULT);
        Check(device->CreateCommittedResource(&h, D3D12_HEAP_FLAG_NONE, &d, D3D12_RESOURCE_STATE_DEPTH_WRITE,
            &clear, IID_PPV_ARGS(&depth)));
        device->CreateDepthStencilView(depth.Get(), nullptr, dsvs->GetCPUDescriptorHandleForHeapStart());
#endif
    }
    void CreatePipeline() {
        D3D12_DESCRIPTOR_RANGE range{}; range.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
        range.NumDescriptors = 1; range.BaseShaderRegister = 0; range.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
        D3D12_ROOT_PARAMETER params[2]{};
        params[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS;
        params[0].Constants.Num32BitValues = 16; params[0].Constants.ShaderRegister = 0;
        params[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;
        params[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
        params[1].DescriptorTable.NumDescriptorRanges = 1; params[1].DescriptorTable.pDescriptorRanges = &range;
        params[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
        D3D12_STATIC_SAMPLER_DESC sampler{}; sampler.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR;
        sampler.AddressU = sampler.AddressV = sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
        sampler.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; sampler.MaxLOD = FLT_MAX;
        sampler.MaxAnisotropy = 1; sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
        D3D12_ROOT_SIGNATURE_DESC d{}; d.NumParameters = TEXTURED ? 2 : 1; d.pParameters = params;
        d.NumStaticSamplers = TEXTURED ? 1 : 0; d.pStaticSamplers = &sampler;
        d.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
        ComPtr<ID3DBlob> blob, error;
        const HRESULT serialized = D3D12SerializeRootSignature(&d, D3D_ROOT_SIGNATURE_VERSION_1, &blob, &error);
        if (error) OutputDebugStringA(static_cast<const char*>(error->GetBufferPointer()));
        Check(serialized);
        Check(device->CreateRootSignature(0, blob->GetBufferPointer(), blob->GetBufferSize(), IID_PPV_ARGS(&root)));
        wchar_t executable[MAX_PATH];
        const DWORD pathLength = GetModuleFileNameW(nullptr, executable, MAX_PATH);
        if (!pathLength || pathLength == MAX_PATH) throw std::runtime_error("Executable path unavailable or exceeds this sample's MAX_PATH limit");
        const auto dir = std::filesystem::path(executable).parent_path() / L"shaders" / (TEXTURED ? L"texture" : L"triangle");
        const auto vs = ReadBytes(dir / L"vs.cso"), ps = ReadBytes(dir / L"ps.cso");
        const D3D12_INPUT_ELEMENT_DESC layout[] = {
            {"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0},
            {"COLOR",0,DXGI_FORMAT_R32G32B32_FLOAT,0,12,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0},
            {"TEXCOORD",0,DXGI_FORMAT_R32G32_FLOAT,0,24,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0}
        };
        D3D12_GRAPHICS_PIPELINE_STATE_DESC p{};
        p.pRootSignature = root.Get(); p.VS = {vs.data(), vs.size()}; p.PS = {ps.data(), ps.size()};
        p.InputLayout = {layout, 3}; p.SampleMask = UINT_MAX;
        auto& blend = p.BlendState.RenderTarget[0];
        blend.SrcBlend = D3D12_BLEND_ONE; blend.DestBlend = D3D12_BLEND_ZERO;
        blend.BlendOp = D3D12_BLEND_OP_ADD; blend.SrcBlendAlpha = D3D12_BLEND_ONE;
        blend.DestBlendAlpha = D3D12_BLEND_ZERO; blend.BlendOpAlpha = D3D12_BLEND_OP_ADD;
        blend.LogicOp = D3D12_LOGIC_OP_NOOP; blend.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
        p.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
        p.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; // Teaching geometry, both sides visible.
        p.RasterizerState.DepthClipEnable = TRUE;
        p.DepthStencilState.DepthEnable = TEXTURED;
        p.DepthStencilState.DepthWriteMask = TEXTURED ? D3D12_DEPTH_WRITE_MASK_ALL : D3D12_DEPTH_WRITE_MASK_ZERO;
        p.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
        p.DepthStencilState.StencilReadMask = p.DepthStencilState.StencilWriteMask = D3D12_DEFAULT_STENCIL_READ_MASK;
        p.DepthStencilState.FrontFace = {D3D12_STENCIL_OP_KEEP,D3D12_STENCIL_OP_KEEP,D3D12_STENCIL_OP_KEEP,D3D12_COMPARISON_FUNC_ALWAYS};
        p.DepthStencilState.BackFace = p.DepthStencilState.FrontFace;
        p.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
        p.NumRenderTargets = 1; p.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
        p.DSVFormat = TEXTURED ? DXGI_FORMAT_D32_FLOAT : DXGI_FORMAT_UNKNOWN; p.SampleDesc.Count = 1;
        Check(device->CreateGraphicsPipelineState(&p, IID_PPV_ARGS(&pipeline)));
    }
    ComPtr<ID3D12Resource> UploadBuffer(const void* data, size_t size) {
        auto h = Heap(D3D12_HEAP_TYPE_UPLOAD); auto d = BufferDesc(size);
        ComPtr<ID3D12Resource> buffer;
        Check(device->CreateCommittedResource(&h, D3D12_HEAP_FLAG_NONE, &d,
            D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&buffer)));
        void* mapped = nullptr; D3D12_RANGE noRead{0,0};
        Check(buffer->Map(0, &noRead, &mapped)); std::memcpy(mapped, data, size);
        D3D12_RANGE written{0,size}; buffer->Unmap(0,&written); return buffer;
    }
    void CreateGeometry() {
        std::vector<Vertex> vertices; std::vector<uint16_t> indices;
#if TEXTURED
        const XMFLOAT3 corners[] = {{-1,-1,-1},{1,-1,-1},{1,1,-1},{-1,1,-1},
                                   {-1,-1,1},{1,-1,1},{1,1,1},{-1,1,1}};
        const int faces[6][4] = {{0,1,2,3},{5,4,7,6},{4,0,3,7},{1,5,6,2},{3,2,6,7},{4,5,1,0}};
        const XMFLOAT2 uvs[] = {{0,1},{1,1},{1,0},{0,0}};
        for (int f = 0; f < 6; ++f) {
            const uint16_t base = static_cast<uint16_t>(vertices.size());
            const float brightness = 0.7f + 0.05f * f; // Face tint, not a lighting system.
            for (int i = 0; i < 4; ++i) vertices.push_back({corners[faces[f][i]],{brightness,brightness,brightness},uvs[i]});
            for (uint16_t i : {0,1,2,0,2,3}) indices.push_back(base+i);
        }
#else
        vertices = {{{0,0.7f,0},{0.6f,0.95f,1},{0,0}},
                    {{0.7f,-0.7f,0},{0.55f,0.6f,1},{1,1}},
                    {{-0.7f,-0.7f,0},{0.3f,0.9f,0.6f},{0,1}}};
        indices = {0,1,2};
#endif
        const UINT vbSize = static_cast<UINT>(vertices.size()*sizeof(Vertex));
        const UINT ibSize = static_cast<UINT>(indices.size()*sizeof(uint16_t));
        vertexBuffer = UploadBuffer(vertices.data(), vbSize); indexBuffer = UploadBuffer(indices.data(),ibSize);
        vertexView = {vertexBuffer->GetGPUVirtualAddress(),vbSize,sizeof(Vertex)};
        indexView = {indexBuffer->GetGPUVirtualAddress(),ibSize,DXGI_FORMAT_R16_UINT};
        indexCount = static_cast<UINT>(indices.size());
    }
    void CreateTexture() {
        const Image image = DecodeImage(imageFile);
        D3D12_RESOURCE_DESC d{}; d.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
        d.Width = image.width; d.Height = image.height; d.DepthOrArraySize = 1; d.MipLevels = 1;
        d.Format = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB; d.SampleDesc.Count = 1;
        auto gpuHeap = Heap(D3D12_HEAP_TYPE_DEFAULT);
        Check(device->CreateCommittedResource(&gpuHeap,D3D12_HEAP_FLAG_NONE,&d,
            D3D12_RESOURCE_STATE_COPY_DEST,nullptr,IID_PPV_ARGS(&texture)));
        D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint{}; UINT rows = 0;
        UINT64 rowBytes = 0, uploadSize = 0;
        device->GetCopyableFootprints(&d,0,1,0,&footprint,&rows,&rowBytes,&uploadSize);
        auto uploadHeap = Heap(D3D12_HEAP_TYPE_UPLOAD); auto uploadDesc = BufferDesc(uploadSize);
        ComPtr<ID3D12Resource> staging;
        Check(device->CreateCommittedResource(&uploadHeap,D3D12_HEAP_FLAG_NONE,&uploadDesc,
            D3D12_RESOURCE_STATE_GENERIC_READ,nullptr,IID_PPV_ARGS(&staging)));
        uint8_t* mapped = nullptr; D3D12_RANGE noRead{0,0};
        Check(staging->Map(0,&noRead,reinterpret_cast<void**>(&mapped)));
        for (UINT y = 0; y < rows; ++y)
            std::memcpy(mapped+footprint.Offset+size_t(y)*footprint.Footprint.RowPitch,
                image.pixels.data()+size_t(y)*image.width*4,static_cast<size_t>(rowBytes));
        D3D12_RANGE written{0,static_cast<SIZE_T>(uploadSize)}; staging->Unmap(0,&written);
        Check(frames[0].allocator->Reset()); Check(list->Reset(frames[0].allocator.Get(),nullptr));
        D3D12_TEXTURE_COPY_LOCATION src{}; src.pResource = staging.Get();
        src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; src.PlacedFootprint = footprint;
        D3D12_TEXTURE_COPY_LOCATION dst{}; dst.pResource = texture.Get(); dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
        list->CopyTextureRegion(&dst,0,0,0,&src,nullptr);
        Transition(list.Get(),texture.Get(),D3D12_RESOURCE_STATE_COPY_DEST,D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
        Check(list->Close()); ID3D12CommandList* commands[] = {list.Get()}; queue->ExecuteCommandLists(1,commands);
        Flush(); // Initialization only: staging stays alive through the completed copy.
        D3D12_SHADER_RESOURCE_VIEW_DESC srv{}; srv.Format = d.Format;
        srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
        srv.Texture2D.MipLevels = 1;
        device->CreateShaderResourceView(texture.Get(),&srv,srvs->GetCPUDescriptorHandleForHeapStart());
    }
    void ResizeIfNeeded() {
        if (!pendingWidth || !pendingHeight || (width == pendingWidth && height == pendingHeight)) return;
        Flush(); for (auto& b : backBuffers) b.Reset(); depth.Reset();
        width = pendingWidth; height = pendingHeight;
        Check(swap->ResizeBuffers(FrameCount,width,height,DXGI_FORMAT_R8G8B8A8_UNORM,0));
        for (auto& f : frames) f.completion = 0;
        CreateTargets();
    }
    void Render() {
        ResizeIfNeeded();
        const UINT index = swap->GetCurrentBackBufferIndex(); auto& frame = frames[index];
        Wait(frame.completion); Check(frame.allocator->Reset()); Check(list->Reset(frame.allocator.Get(),pipeline.Get()));
        Transition(list.Get(),backBuffers[index].Get(),D3D12_RESOURCE_STATE_PRESENT,D3D12_RESOURCE_STATE_RENDER_TARGET);
        const auto rtv = Rtv(index); const auto dsv = dsvs->GetCPUDescriptorHandleForHeapStart();
        const float clear[] = {0.01f,0.025f,0.045f,1}; list->ClearRenderTargetView(rtv,clear,0,nullptr);
        list->OMSetRenderTargets(1,&rtv,FALSE,TEXTURED?&dsv:nullptr);
#if TEXTURED
        list->ClearDepthStencilView(dsv,D3D12_CLEAR_FLAG_DEPTH,1.0f,0,0,nullptr);
#endif
        const D3D12_VIEWPORT viewport{0,0,float(width),float(height),0,1};
        const D3D12_RECT scissor{0,0,LONG(width),LONG(height)};
        list->RSSetViewports(1,&viewport); list->RSSetScissorRects(1,&scissor);
        list->SetGraphicsRootSignature(root.Get());
        XMMATRIX matrix = XMMatrixIdentity();
#if TEXTURED
        const float t = std::chrono::duration<float>(std::chrono::steady_clock::now()-start).count();
        matrix = XMMatrixRotationY(t*0.45f) * XMMatrixRotationX(0.3f)
            * XMMatrixLookAtLH(XMVectorSet(0,1,-5,1),XMVectorZero(),XMVectorSet(0,1,0,0))
            * XMMatrixPerspectiveFovLH(XM_PIDIV4,float(width)/height,0.1f,100.0f);
        ID3D12DescriptorHeap* heaps[] = {srvs.Get()}; list->SetDescriptorHeaps(1,heaps);
        list->SetGraphicsRootDescriptorTable(1,srvs->GetGPUDescriptorHandleForHeapStart());
#endif
        XMFLOAT4X4 stored; XMStoreFloat4x4(&stored,matrix);
        list->SetGraphicsRoot32BitConstants(0,16,&stored,0);
        list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
        list->IASetVertexBuffers(0,1,&vertexView); list->IASetIndexBuffer(&indexView);
        list->DrawIndexedInstanced(indexCount,1,0,0,0);
        Transition(list.Get(),backBuffers[index].Get(),D3D12_RESOURCE_STATE_RENDER_TARGET,D3D12_RESOURCE_STATE_PRESENT);
        Check(list->Close()); ID3D12CommandList* commands[] = {list.Get()}; queue->ExecuteCommandLists(1,commands);
        Check(swap->Present(1,0)); frame.completion = Signal();
    }
};
LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w, LPARAM l) {
    auto* app = reinterpret_cast<Sample*>(GetWindowLongPtrW(window,GWLP_USERDATA));
    if (message == WM_NCCREATE) {
        app = static_cast<Sample*>(reinterpret_cast<CREATESTRUCTW*>(l)->lpCreateParams);
        SetWindowLongPtrW(window,GWLP_USERDATA,reinterpret_cast<LONG_PTR>(app));
    }
    if (message == WM_SIZE && app) {
        app->minimized = w == SIZE_MINIMIZED;
        app->pendingWidth = LOWORD(l); app->pendingHeight = HIWORD(l); return 0;
    }
    if (message == WM_KEYDOWN && w == VK_ESCAPE) { DestroyWindow(window); return 0; }
    if (message == WM_DESTROY) { PostQuitMessage(0); return 0; }
    if (message == WM_PAINT) { PAINTSTRUCT ps; BeginPaint(window,&ps); EndPaint(window,&ps); return 0; }
    return DefWindowProcW(window,message,w,l);
}
}
int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,LPWSTR,int show) {
    bool comInitialized = false;
    int result = 0;
    try {
        Check(CoInitializeEx(nullptr,COINIT_MULTITHREADED)); comInitialized = true;
        Sample app;
        int argc = 0; LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(),&argc);
        if (!argv) throw std::runtime_error("Command-line parsing failed");
        for (int i = 1; i < argc; ++i) {
            if (std::wstring(argv[i]) == L"--warp") app.warp = true;
            else app.imageFile = argv[i];
        }
        LocalFree(argv);
        WNDCLASSW cls{}; cls.lpfnWndProc = WindowProc; cls.hInstance = instance;
        cls.lpszClassName = L"QubicTeachingWindow"; cls.hCursor = LoadCursor(nullptr,IDC_ARROW);
        if (!RegisterClassW(&cls)) throw std::runtime_error("Window class registration failed");
        RECT rectangle{0,0,LONG(app.width),LONG(app.height)};
        AdjustWindowRect(&rectangle,WS_OVERLAPPEDWINDOW,FALSE);
        app.window = CreateWindowExW(0,cls.lpszClassName,TEXTURED?L"Qubic DX12 — texture sample":L"Qubic DX12 — first triangle",
            WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,rectangle.right-rectangle.left,
            rectangle.bottom-rectangle.top,nullptr,nullptr,instance,&app);
        if (!app.window) throw std::runtime_error("Window creation failed");
        app.Init(); ShowWindow(app.window,show);
        MSG message{};
        while (message.message != WM_QUIT) {
            if (PeekMessageW(&message,nullptr,0,0,PM_REMOVE)) { TranslateMessage(&message); DispatchMessageW(&message); }
            else if (app.minimized) WaitMessage();
            else app.Render();
        }
        app.Flush();
    } catch (const std::exception& error) {
        MessageBoxA(nullptr,error.what(),"Qubic DX12 sample",MB_OK|MB_ICONERROR); result = 1;
    }
    if (comInitialized) CoUninitialize();
    return result;
}
Complete sample source: dx12/Shaders/Scene.hlsl
// C++20 sample contract: row vectors, row-major matrices, left-handed camera.
// This matrix is sixteen root constants, not a separately allocated CBV.
cbuffer FrameConstants : register(b0) {
    row_major float4x4 worldViewProjection;
};
#if TEXTURED
Texture2D<float4> baseColor : register(t0);
SamplerState linearSampler : register(s0);
#endif
struct VertexInput {
    float3 position : POSITION;
    float3 color : COLOR;
    float2 uv : TEXCOORD;
};
struct VertexOutput {
    float4 position : SV_Position;
    float3 color : COLOR;
    float2 uv : TEXCOORD;
};
VertexOutput VSMain(VertexInput input) {
    VertexOutput output;
    output.position = mul(float4(input.position, 1.0), worldViewProjection);
    output.color = input.color;
    output.uv = input.uv;
    return output;
}
float4 PSMain(VertexOutput input) : SV_Target {
#if TEXTURED
    // SRGB SRV decodes color; SRGB RTV encodes output for display.
    return float4(baseColor.Sample(linearSampler, input.uv).rgb * input.color, 1.0);
#else
    return float4(input.color, 1.0);
#endif
}

Beyond one mip#

Production import cooks mipmaps, format conversions, and GPU compression. Multiple mips are separate subresources with their own footprints. Arrays and compressed formats require the actual returned layouts; a hardcoded four-byte pixel assumption is not general. The sampler’s mip range must match populated levels. Normal maps, roughness, and base color need different filtering/color conventions. Streaming may publish a ready lower-detail asset version first, then update residency while retaining safe bindings. The asset system chapter defines those states.

Debug the symptom#

Corrupted rows: compare decodedStride, rowBytes, and RowPitch. Black output: inspect decode errors, SRV register/table index, heap binding, and resource state. Wrong color: check sRGB versus linear interpretation and output encoding. Sporadic corruption: inspect staging/descriptor lifetimes and completion values. Native compilation and output remain unverified in this Linux environment. Microsoft’s upload guide verifies the API concepts; the Windows sample checklist verifies the actual executable once you run it.

Search titles and full article text.