DirectX 12 / PRIMARY BACKEND

Your first triangle

Build the full pipeline contract: shader inputs, root constants, pipeline state, vertex/index data, command recording, and a visible draw.

Start from the shared sample#

Finish setup and read device creation. The relevant files are samples/dx12/main.cpp and Shaders/Scene.hlsl. Build qubic_triangle from CMake rather than pasting a draw call into an unrelated application.

text · REFERENCE EXCERPT
cmake --build build --config Debug --target qubic_triangle
build\Debug\qubic_triangle.exe

Expected output is a color-interpolated triangle on a dark background. The source below is complete for the teaching sample, including the window, initialization, shaders, resource ownership, resizing, and shutdown. It has not been natively compiled in the authoring environment.

What the pipeline agrees about#

Vertex contains position float3, color float3, and UV float2. The input layout uses byte offsets 0, 12, and 24 and a stride of 32 bytes. Three vertices and three 16-bit indices define the triangle. UploadBuffer is fully defined in main.cpp and creates a mapped upload-heap buffer in GENERIC_READ state. The root signature binds sixteen 32-bit root constants at b0, visible to the vertex shader. The matrix is identity for the triangle. The texture target extends that signature with a descriptor table at t0 and a static sampler at s0. The triangle variant does not reference those bindings. The pipeline state combines the root signature, compiled vertex/pixel shaders, vertex layout, triangle topology, raster state, blending, depth state, and attachment formats. It is created once, not reconstructed for every draw. The triangle sample intentionally disables depth and back-face culling to keep coverage easy to inspect.

INTERACTIVE ILLUSTRATIONSIMULATION / BROWSER

A triangle becomes pixels 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 ↗

Read the shader#

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
}

VSMain multiplies a row-vector position by a row-major matrix and writes SV_Position. Color and UV pass through as interpolated attributes. PSMain returns the interpolated color in the triangle variant. Shader compilation produces DXIL; a native DX12 pipeline consumes that bytecode, while the browser lab uses JavaScript algorithms.

Record the draw in order#

  1. Wait for the chosen frame context’s previous fence and reset its allocator and command list.
  2. Transition the current back buffer to RENDER_TARGET and clear it.
  3. Set RTV, viewport, scissor, pipeline, and root signature.
  4. Copy the identity matrix into root constants, set topology, and bind vertex/index views.
  5. DrawIndexedInstanced with three indices and one instance.
  6. Transition to PRESENT, close and execute the list, present, and signal completion.

Root constants copy their values into recorded command state. Upload buffers stay owned by Sample until shutdown. Command allocator storage stays owned by a FrameContext and is reset only after its completion fence passes.

Complete native source#

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;
}

If the triangle is missing#

Confirm shaders compiled and were copied next to the executable. Check the PSO render-target format against the RTV. Verify vertex stride, input offsets, index format, viewport dimensions, and DrawIndexedInstanced count. Reduce the shader to a constant output color to isolate interpolation from resource binding. A compiler error and a graphics validation error require different investigation. Read the DXC diagnostic for shader syntax or target support; read the DX12 debug-layer output for root signature, resource state, and command validity. Do not interpret the browser illustration as proof that native commands run successfully.

Extend the same path#

Resources and descriptors explains the next bindings. Texture loading builds qubic_texture with the same native owner and frame loop, adding a real camera, indexed cube, depth buffer, decoded pixels, transfer, and SRV.

Search titles and full article text.