Skip to content

Instantly share code, notes, and snippets.

@jakubtomsu
Last active January 6, 2025 09:15
Show Gist options
  • Save jakubtomsu/ecd83e61976d974c7730f9d7ad3e1fd0 to your computer and use it in GitHub Desktop.
Save jakubtomsu/ecd83e61976d974c7730f9d7ad3e1fd0 to your computer and use it in GitHub Desktop.
Simple d3d12 triangle example in Odin
// D3D12 single-function triangle sample.
//
// Usage:
// - copy SDL2.dll from Odin/vendor/sdl2 to your project directory
// - odin run .
//
// Contributors:
// - Jakub Tomšů (updated to newest Odin version)
// - Karl Zylinski <[email protected]> (Initial port)
//
// Based on:
// - https://gist.github.com/karl-zylinski/e1d1d0925ac5db0f12e4837435c5bbfb
// - https://github.com/rdunnington/d3d12-hello-triangle/blob/master/main.c
package d3d12_triangle
import "core:fmt"
import "core:mem"
import "core:sys/windows"
import "core:os"
import sdl "vendor:sdl2"
import d3d12 "vendor:directx/d3d12"
import dxgi "vendor:directx/dxgi"
import d3dc "vendor:directx/d3d_compiler"
NUM_RENDERTARGETS :: 2
check :: proc(res: d3d12.HRESULT, message: string) {
if (res >= 0) {
return
}
fmt.printf("%v. Error code: %0x\n", message, u32(res))
os.exit(-1)
}
main :: proc() {
// Init SDL and create window
if err := sdl.Init({.VIDEO}); err != 0 {
fmt.eprintln(err)
return
}
defer sdl.Quit()
wx := i32(640)
wy := i32(480)
window := sdl.CreateWindow("d3d12 triangle", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, wx, wy, { .ALLOW_HIGHDPI, .SHOWN, .RESIZABLE })
if window == nil {
fmt.eprintln(sdl.GetError())
return
}
defer sdl.DestroyWindow(window)
hr: d3d12.HRESULT
// Init DXGI factory. DXGI is the link between the window and DirectX
factory: ^dxgi.IFactory4
{
flags: u32 = 0
when ODIN_DEBUG {
flags |= dxgi.CREATE_FACTORY_DEBUG
}
hr = dxgi.CreateDXGIFactory2(flags, dxgi.IFactory4_UUID, cast(^rawptr)&factory)
check(hr, "Failed creating factory")
}
// Find the DXGI adapter (GPU)
adapter: ^dxgi.IAdapter1
error_not_found := dxgi.HRESULT(-142213123)
for i: u32 = 0; factory->EnumAdapters1(i, &adapter) != error_not_found; i += 1 {
desc: dxgi.ADAPTER_DESC1
adapter->GetDesc1(&desc)
if desc.Flags & u32(dxgi.ADAPTER_FLAG.SOFTWARE) != 0 {
continue
}
if d3d12.CreateDevice((^dxgi.IUnknown)(adapter), ._12_0, dxgi.IDevice_UUID, nil) >= 0 {
break
} else {
fmt.println("Failed to create device")
}
}
if adapter == nil {
fmt.println("Could not find hardware adapter")
return
}
// Create D3D12 device that represents the GPU
device: ^d3d12.IDevice
hr = d3d12.CreateDevice((^dxgi.IUnknown)(adapter), ._12_0, d3d12.IDevice_UUID, (^rawptr)(&device))
check(hr, "Failed to create device")
queue: ^d3d12.ICommandQueue
{
desc := d3d12.COMMAND_QUEUE_DESC {
Type = .DIRECT,
}
hr = device->CreateCommandQueue(&desc, d3d12.ICommandQueue_UUID, (^rawptr)(&queue))
check(hr, "Failed creating command queue")
}
// Get the window handle from SDL
window_info: sdl.SysWMinfo
sdl.GetWindowWMInfo(window, &window_info)
window_handle := dxgi.HWND(window_info.info.win.window)
// Create the swapchain, it's the thing that contains render targets that we draw into. It has 2 render targets (NUM_RENDERTARGETS), giving us double buffering.
swapchain: ^dxgi.ISwapChain3
{
desc := dxgi.SWAP_CHAIN_DESC1 {
Width = u32(wx),
Height = u32(wy),
Format = .R8G8B8A8_UNORM,
SampleDesc = {
Count = 1,
Quality = 0,
},
BufferUsage = {.RENDER_TARGET_OUTPUT},
BufferCount = NUM_RENDERTARGETS,
Scaling = .NONE,
SwapEffect = .FLIP_DISCARD,
AlphaMode = .UNSPECIFIED,
}
hr = factory->CreateSwapChainForHwnd((^dxgi.IUnknown)(queue), window_handle, &desc, nil, nil, (^^dxgi.ISwapChain1)(&swapchain))
check(hr, "Failed to create swap chain")
}
frame_index := swapchain->GetCurrentBackBufferIndex()
// Descripors describe the GPU data and are allocated from a Descriptor Heap
rtv_descriptor_heap: ^d3d12.IDescriptorHeap
{
desc := d3d12.DESCRIPTOR_HEAP_DESC {
NumDescriptors = NUM_RENDERTARGETS,
Type = .RTV,
Flags = {},
}
hr = device->CreateDescriptorHeap(&desc, d3d12.IDescriptorHeap_UUID, (^rawptr)(&rtv_descriptor_heap))
check(hr, "Failed creating descriptor heap")
}
// Fetch the two render targets from the swapchain
targets: [NUM_RENDERTARGETS]^d3d12.IResource
{
rtv_descriptor_size: u32 = device->GetDescriptorHandleIncrementSize(.RTV)
rtv_descriptor_handle: d3d12.CPU_DESCRIPTOR_HANDLE
rtv_descriptor_heap->GetCPUDescriptorHandleForHeapStart(&rtv_descriptor_handle)
for i :u32= 0; i < NUM_RENDERTARGETS; i += 1 {
hr = swapchain->GetBuffer(i, d3d12.IResource_UUID, (^rawptr)(&targets[i]))
check(hr, "Failed getting render target")
device->CreateRenderTargetView(targets[i], nil, rtv_descriptor_handle)
rtv_descriptor_handle.ptr += uint(rtv_descriptor_size)
}
}
// The command allocator is used to create the commandlist that is used to tell the GPU what to draw
command_allocator: ^d3d12.ICommandAllocator
hr = device->CreateCommandAllocator(.DIRECT, d3d12.ICommandAllocator_UUID, (^rawptr)(&command_allocator))
check(hr, "Failed creating command allocator")
/*
From https://docs.microsoft.com/en-us/windows/win32/direct3d12/root-signatures-overview:
A root signature is configured by the app and links command lists to the resources the shaders require.
The graphics command list has both a graphics and compute root signature. A compute command list will
simply have one compute root signature. These root signatures are independent of each other.
*/
root_signature: ^d3d12.IRootSignature
{
desc := d3d12.VERSIONED_ROOT_SIGNATURE_DESC {
Version = ._1_0,
}
desc.Desc_1_0.Flags = {.ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT}
serialized_desc: ^d3d12.IBlob
hr = d3d12.SerializeVersionedRootSignature(&desc, &serialized_desc, nil)
check(hr, "Failed to serialize root signature")
hr = device->CreateRootSignature(0, serialized_desc->GetBufferPointer(), serialized_desc->GetBufferSize(), d3d12.IRootSignature_UUID, (^rawptr)(&root_signature))
check(hr, "Failed creating root signature")
serialized_desc->Release()
}
// The pipeline contains the shaders etc to use
pipeline: ^d3d12.IPipelineState
{
// Compile vertex and pixel shaders
data :cstring=
`struct PSInput {
float4 position : SV_POSITION;
float4 color : COLOR;
};
PSInput VSMain(float4 position : POSITION0, float4 color : COLOR0) {
PSInput result;
result.position = position;
result.color = color;
return result;
}
float4 PSMain(PSInput input) : SV_TARGET {
return input.color;
};`
data_size: uint = len(data)
compile_flags: u32 = 0
when ODIN_DEBUG {
compile_flags |= u32(d3dc.D3DCOMPILE.DEBUG)
compile_flags |= u32(d3dc.D3DCOMPILE.SKIP_OPTIMIZATION)
}
vs: ^d3d12.IBlob = nil
ps: ^d3d12.IBlob = nil
hr = d3dc.Compile(rawptr(data), data_size, nil, nil, nil, "VSMain", "vs_4_0", compile_flags, 0, &vs, nil)
check(hr, "Failed to compile vertex shader")
hr =d3dc.Compile(rawptr(data), data_size, nil, nil, nil, "PSMain", "ps_4_0", compile_flags, 0, &ps, nil)
check(hr, "Failed to compile pixel shader")
// This layout matches the vertices data defined further down
vertex_format: []d3d12.INPUT_ELEMENT_DESC = {
{
SemanticName = "POSITION",
Format = .R32G32B32_FLOAT,
InputSlotClass = .PER_VERTEX_DATA,
},
{
SemanticName = "COLOR",
Format = .R32G32B32A32_FLOAT,
AlignedByteOffset = size_of(f32) * 3,
InputSlotClass = .PER_VERTEX_DATA,
},
}
default_blend_state := d3d12.RENDER_TARGET_BLEND_DESC {
BlendEnable = false,
LogicOpEnable = false,
SrcBlend = .ONE,
DestBlend = .ZERO,
BlendOp = .ADD,
SrcBlendAlpha = .ONE,
DestBlendAlpha = .ZERO,
BlendOpAlpha = .ADD,
LogicOp = .NOOP,
RenderTargetWriteMask = u8(d3d12.COLOR_WRITE_ENABLE_ALL),
}
pipeline_state_desc := d3d12.GRAPHICS_PIPELINE_STATE_DESC {
pRootSignature = root_signature,
VS = {
pShaderBytecode = vs->GetBufferPointer(),
BytecodeLength = vs->GetBufferSize(),
},
PS = {
pShaderBytecode = ps->GetBufferPointer(),
BytecodeLength = ps->GetBufferSize(),
},
StreamOutput = {},
BlendState = {
AlphaToCoverageEnable = false,
IndependentBlendEnable = false,
RenderTarget = { 0 = default_blend_state, 1..<7 = {} },
},
SampleMask = 0xFFFFFFFF,
RasterizerState = {
FillMode = .SOLID,
CullMode = .BACK,
FrontCounterClockwise = false,
DepthBias = 0,
DepthBiasClamp = 0,
SlopeScaledDepthBias = 0,
DepthClipEnable = true,
MultisampleEnable = false,
AntialiasedLineEnable = false,
ForcedSampleCount = 0,
ConservativeRaster = .OFF,
},
DepthStencilState = {
DepthEnable = false,
StencilEnable = false,
},
InputLayout = {
pInputElementDescs = &vertex_format[0],
NumElements = u32(len(vertex_format)),
},
PrimitiveTopologyType = .TRIANGLE,
NumRenderTargets = 1,
RTVFormats = { 0 = .R8G8B8A8_UNORM, 1..<7 = .UNKNOWN },
DSVFormat = .UNKNOWN,
SampleDesc = {
Count = 1,
Quality = 0,
},
}
hr = device->CreateGraphicsPipelineState(&pipeline_state_desc, d3d12.IPipelineState_UUID, (^rawptr)(&pipeline))
check(hr, "Pipeline creation failed")
vs->Release()
ps->Release()
}
// Create the commandlist that is reused further down.
cmdlist: ^d3d12.IGraphicsCommandList
hr = device->CreateCommandList(0, .DIRECT, command_allocator, pipeline, d3d12.ICommandList_UUID, (^rawptr)(&cmdlist))
check(hr, "Failed to create command list")
hr = cmdlist->Close()
check(hr, "Failed to close command list")
vertex_buffer: ^d3d12.IResource
vertex_buffer_view: d3d12.VERTEX_BUFFER_VIEW
{
// The position and color data for the triangle's vertices go together per-vertex
vertices := [?]f32 {
// pos color
0.0 , 0.5, 0.0, 1,0,0,0,
0.5, -0.5, 0.0, 0,1,0,0,
-0.5, -0.5, 0.0, 0,0,1,0,
}
heap_props := d3d12.HEAP_PROPERTIES {
Type = .UPLOAD,
}
vertex_buffer_size := len(vertices) * size_of(vertices[0])
resource_desc := d3d12.RESOURCE_DESC {
Dimension = .BUFFER,
Alignment = 0,
Width = u64(vertex_buffer_size),
Height = 1,
DepthOrArraySize = 1,
MipLevels = 1,
Format = .UNKNOWN,
SampleDesc = { Count = 1, Quality = 0 },
Layout = .ROW_MAJOR,
Flags = {},
}
hr = device->CreateCommittedResource(&heap_props, {}, &resource_desc, d3d12.RESOURCE_STATE_GENERIC_READ, nil, d3d12.IResource_UUID, (^rawptr)(&vertex_buffer))
check(hr, "Failed creating vertex buffer")
gpu_data: rawptr
read_range: d3d12.RANGE
hr = vertex_buffer->Map(0, &read_range, &gpu_data)
check(hr, "Failed creating verex buffer resource")
mem.copy(gpu_data, &vertices[0], vertex_buffer_size)
vertex_buffer->Unmap(0, nil)
vertex_buffer_view = d3d12.VERTEX_BUFFER_VIEW {
BufferLocation = vertex_buffer->GetGPUVirtualAddress(),
StrideInBytes = u32(vertex_buffer_size/3),
SizeInBytes = u32(vertex_buffer_size),
}
}
// This fence is used to wait for frames to finish
fence_value: u64
fence: ^d3d12.IFence
fence_event: windows.HANDLE
{
hr = device->CreateFence(fence_value, {}, d3d12.IFence_UUID, (^rawptr)(&fence))
check(hr, "Failed to create fence")
fence_value += 1
manual_reset: windows.BOOL = false
initial_state: windows.BOOL = false
fence_event = windows.CreateEventW(nil, manual_reset, initial_state, nil)
if fence_event == nil {
fmt.println("Failed to create fence event")
return
}
}
main_loop: for {
for e: sdl.Event; sdl.PollEvent(&e); {
#partial switch e.type {
case .QUIT:
break main_loop
case .WINDOWEVENT:
// This is equivalent to WM_PAINT in win32 API
if e.window.event == .EXPOSED {
hr = command_allocator->Reset()
check(hr, "Failed resetting command allocator")
hr = cmdlist->Reset(command_allocator, pipeline)
check(hr, "Failed to reset command list")
viewport := d3d12.VIEWPORT {
Width = f32(wx),
Height = f32(wy),
}
scissor_rect := d3d12.RECT {
left = 0, right = wx,
top = 0, bottom = wy,
}
// This state is reset everytime the cmd list is reset, so we need to rebind it
cmdlist->SetGraphicsRootSignature(root_signature)
cmdlist->RSSetViewports(1, &viewport)
cmdlist->RSSetScissorRects(1, &scissor_rect)
to_render_target_barrier := d3d12.RESOURCE_BARRIER {
Type = .TRANSITION,
Flags = {},
}
to_render_target_barrier.Transition = {
pResource = targets[frame_index],
StateBefore = d3d12.RESOURCE_STATE_PRESENT,
StateAfter = {.RENDER_TARGET},
Subresource = d3d12.RESOURCE_BARRIER_ALL_SUBRESOURCES,
}
cmdlist->ResourceBarrier(1, &to_render_target_barrier)
rtv_handle: d3d12.CPU_DESCRIPTOR_HANDLE
rtv_descriptor_heap->GetCPUDescriptorHandleForHeapStart(&rtv_handle)
if (frame_index > 0) {
s := device->GetDescriptorHandleIncrementSize(.RTV)
rtv_handle.ptr += uint(frame_index * s)
}
cmdlist->OMSetRenderTargets(1, &rtv_handle, false, nil)
// clear backbuffer
clearcolor := [?]f32 { 0.05, 0.05, 0.05, 1.0 }
cmdlist->ClearRenderTargetView(rtv_handle, &clearcolor, 0, nil)
// draw call
cmdlist->IASetPrimitiveTopology(.TRIANGLELIST)
cmdlist->IASetVertexBuffers(0, 1, &vertex_buffer_view)
cmdlist->DrawInstanced(3, 1, 0, 0)
to_present_barrier := to_render_target_barrier
to_present_barrier.Transition.StateBefore = {.RENDER_TARGET}
to_present_barrier.Transition.StateAfter = d3d12.RESOURCE_STATE_PRESENT
cmdlist->ResourceBarrier(1, &to_present_barrier)
hr = cmdlist->Close()
check(hr, "Failed to close command list")
// execute
cmdlists := [?]^d3d12.IGraphicsCommandList { cmdlist }
queue->ExecuteCommandLists(len(cmdlists), (^^d3d12.ICommandList)(&cmdlists[0]))
// present
{
flags: u32
params: dxgi.PRESENT_PARAMETERS
hr = swapchain->Present1(1, flags, &params)
check(hr, "Present failed")
}
// wait for frame to finish
{
current_fence_value := fence_value
hr = queue->Signal(fence, current_fence_value)
check(hr, "Failed to signal fence")
fence_value += 1
completed := fence->GetCompletedValue()
if completed < current_fence_value {
hr = fence->SetEventOnCompletion(current_fence_value, fence_event)
check(hr, "Failed to set event on completion flag")
windows.WaitForSingleObject(fence_event, windows.INFINITE)
}
frame_index = swapchain->GetCurrentBackBufferIndex()
}
}
}
}
}
}
@Hyrtwol
Copy link

Hyrtwol commented Oct 11, 2024

Just what I looked for, tx for sharing :)

@rupakhetibinit
Copy link

I needed to change lines 62, 79 and 474 to handle bitsets correctly

        flags: dxgi.CREATE_FACTORY = { .DEBUG }
        if dxgi.ADAPTER_FLAG.SOFTWARE in desc.Flags {
        continue
}
// present
{
        flags: dxgi.PRESENT
        params: dxgi.PRESENT_PARAMETERS
        hr = swapchain->Present1(1, flags, &params)
        check(hr, "Present failed")
}

Full code

// D3D12 single-function triangle sample.
//
// Usage:
// - copy SDL2.dll from Odin/vendor/sdl2 to your project directory
// - odin run .
//
// Contributors:
// - Jakub Tomšů (updated to newest Odin version)
// - Karl Zylinski <[email protected]> (Initial port)
//
// Based on:
// - https://gist.github.com/karl-zylinski/e1d1d0925ac5db0f12e4837435c5bbfb
// - https://github.com/rdunnington/d3d12-hello-triangle/blob/master/main.c

package d3d12_triangle

import "core:fmt"
import "core:mem"
import "core:sys/windows"
import "core:os"
import sdl "vendor:sdl2"
import d3d12 "vendor:directx/d3d12"
import dxgi "vendor:directx/dxgi"
import d3dc "vendor:directx/d3d_compiler"

NUM_RENDERTARGETS :: 2

check :: proc(res: d3d12.HRESULT, message: string) {
    if (res >= 0) {
        return
    }

    fmt.printf("%v. Error code: %0x\n", message, u32(res))
    os.exit(-1)
}

main :: proc() {
    // Init SDL and create window

    if err := sdl.Init({.VIDEO}); err != 0 {
        fmt.eprintln(err)
        return
    }

    defer sdl.Quit()
    wx := i32(640)
    wy := i32(480)
    window := sdl.CreateWindow("d3d12 triangle", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, wx, wy, { .ALLOW_HIGHDPI, .SHOWN, .RESIZABLE })

    if window == nil {
        fmt.eprintln(sdl.GetError())
        return
    }

    defer sdl.DestroyWindow(window)
    hr: d3d12.HRESULT

    // Init DXGI factory. DXGI is the link between the window and DirectX
    factory: ^dxgi.IFactory4

    {
        flags: dxgi.CREATE_FACTORY = { .DEBUG }

        when ODIN_DEBUG {
            flags |= dxgi.CREATE_FACTORY_DEBUG
        }

        hr = dxgi.CreateDXGIFactory2(flags, dxgi.IFactory4_UUID, cast(^rawptr)&factory)
        check(hr, "Failed creating factory")
    }

    // Find the DXGI adapter (GPU)
    adapter: ^dxgi.IAdapter1
    error_not_found := dxgi.HRESULT(-142213123)

    for i: u32 = 0; factory->EnumAdapters1(i, &adapter) != error_not_found; i += 1 {
        desc: dxgi.ADAPTER_DESC1
        adapter->GetDesc1(&desc)
        if dxgi.ADAPTER_FLAG.SOFTWARE in desc.Flags {
            continue
        }

        if d3d12.CreateDevice((^dxgi.IUnknown)(adapter), ._12_0, dxgi.IDevice_UUID, nil) >= 0 {
            break
        } else {
            fmt.println("Failed to create device")
        }
    }

    if adapter == nil {
        fmt.println("Could not find hardware adapter")
        return
    }

    // Create D3D12 device that represents the GPU
    device: ^d3d12.IDevice
    hr = d3d12.CreateDevice((^dxgi.IUnknown)(adapter), ._12_0, d3d12.IDevice_UUID, (^rawptr)(&device))
    check(hr, "Failed to create device")
    queue: ^d3d12.ICommandQueue

    {
        desc := d3d12.COMMAND_QUEUE_DESC {
            Type = .DIRECT,
        }

        hr = device->CreateCommandQueue(&desc, d3d12.ICommandQueue_UUID, (^rawptr)(&queue))
        check(hr, "Failed creating command queue")
    }

    // Get the window handle from SDL
    window_info: sdl.SysWMinfo
    sdl.GetWindowWMInfo(window, &window_info)
    window_handle := dxgi.HWND(window_info.info.win.window)
    
    // Create the swapchain, it's the thing that contains render targets that we draw into. It has 2 render targets (NUM_RENDERTARGETS), giving us double buffering.
    swapchain: ^dxgi.ISwapChain3
    
    {
        desc := dxgi.SWAP_CHAIN_DESC1 {
            Width = u32(wx),
            Height = u32(wy),
            Format = .R8G8B8A8_UNORM,
            SampleDesc = {
                Count = 1,
                Quality = 0,
            },
            BufferUsage = {.RENDER_TARGET_OUTPUT},
            BufferCount = NUM_RENDERTARGETS,
            Scaling = .NONE,
            SwapEffect = .FLIP_DISCARD,
            AlphaMode = .UNSPECIFIED,
        }

        hr = factory->CreateSwapChainForHwnd((^dxgi.IUnknown)(queue), window_handle, &desc, nil, nil, (^^dxgi.ISwapChain1)(&swapchain))
        check(hr, "Failed to create swap chain")
    }

    frame_index := swapchain->GetCurrentBackBufferIndex()

    // Descripors describe the GPU data and are allocated from a Descriptor Heap
    rtv_descriptor_heap: ^d3d12.IDescriptorHeap

    {
        desc := d3d12.DESCRIPTOR_HEAP_DESC {
            NumDescriptors = NUM_RENDERTARGETS,
            Type = .RTV,
            Flags = {},
        }

        hr = device->CreateDescriptorHeap(&desc, d3d12.IDescriptorHeap_UUID, (^rawptr)(&rtv_descriptor_heap))
        check(hr, "Failed creating descriptor heap")
    }

    // Fetch the two render targets from the swapchain
    targets: [NUM_RENDERTARGETS]^d3d12.IResource

    {
        rtv_descriptor_size: u32 = device->GetDescriptorHandleIncrementSize(.RTV)

        rtv_descriptor_handle: d3d12.CPU_DESCRIPTOR_HANDLE
        rtv_descriptor_heap->GetCPUDescriptorHandleForHeapStart(&rtv_descriptor_handle)

        for i :u32= 0; i < NUM_RENDERTARGETS; i += 1 {
            hr = swapchain->GetBuffer(i, d3d12.IResource_UUID, (^rawptr)(&targets[i]))
            check(hr, "Failed getting render target")
            device->CreateRenderTargetView(targets[i], nil, rtv_descriptor_handle)
            rtv_descriptor_handle.ptr += uint(rtv_descriptor_size)
        }
    }

    // The command allocator is used to create the commandlist that is used to tell the GPU what to draw
    command_allocator: ^d3d12.ICommandAllocator
    hr = device->CreateCommandAllocator(.DIRECT, d3d12.ICommandAllocator_UUID, (^rawptr)(&command_allocator))
    check(hr, "Failed creating command allocator")

    /* 
    From https://docs.microsoft.com/en-us/windows/win32/direct3d12/root-signatures-overview:
    
        A root signature is configured by the app and links command lists to the resources the shaders require.
        The graphics command list has both a graphics and compute root signature. A compute command list will
        simply have one compute root signature. These root signatures are independent of each other.
    */
    root_signature: ^d3d12.IRootSignature 

    {
        desc := d3d12.VERSIONED_ROOT_SIGNATURE_DESC {
            Version = ._1_0,
        }

        desc.Desc_1_0.Flags = {.ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT}
        serialized_desc: ^d3d12.IBlob
        hr = d3d12.SerializeVersionedRootSignature(&desc, &serialized_desc, nil)
        check(hr, "Failed to serialize root signature")
        hr = device->CreateRootSignature(0, serialized_desc->GetBufferPointer(), serialized_desc->GetBufferSize(), d3d12.IRootSignature_UUID, (^rawptr)(&root_signature))
        check(hr, "Failed creating root signature")
        serialized_desc->Release()
    }

    // The pipeline contains the shaders etc to use
    pipeline: ^d3d12.IPipelineState

    {
        // Compile vertex and pixel shaders
        data :cstring=
            `struct PSInput {
               float4 position : SV_POSITION;
               float4 color : COLOR;
            };
            PSInput VSMain(float4 position : POSITION0, float4 color : COLOR0) {
               PSInput result;
               result.position = position;
               result.color = color;
               return result;
            }
            float4 PSMain(PSInput input) : SV_TARGET {
               return input.color;
            };`

        data_size: uint = len(data)

        compile_flags: u32 = 0
        when ODIN_DEBUG {
            compile_flags |= u32(d3dc.D3DCOMPILE.DEBUG)
            compile_flags |= u32(d3dc.D3DCOMPILE.SKIP_OPTIMIZATION)
        }

        vs: ^d3d12.IBlob = nil
        ps: ^d3d12.IBlob = nil

        hr = d3dc.Compile(rawptr(data), data_size, nil, nil, nil, "VSMain", "vs_4_0", compile_flags, 0, &vs, nil)
        check(hr, "Failed to compile vertex shader")

        hr =d3dc.Compile(rawptr(data), data_size, nil, nil, nil, "PSMain", "ps_4_0", compile_flags, 0, &ps, nil)
        check(hr, "Failed to compile pixel shader")

        // This layout matches the vertices data defined further down
        vertex_format: []d3d12.INPUT_ELEMENT_DESC = {
            { 
                SemanticName = "POSITION", 
                Format = .R32G32B32_FLOAT, 
                InputSlotClass = .PER_VERTEX_DATA, 
            },
            {   
                SemanticName = "COLOR", 
                Format = .R32G32B32A32_FLOAT, 
                AlignedByteOffset = size_of(f32) * 3, 
                InputSlotClass = .PER_VERTEX_DATA, 
            },
        }

        default_blend_state := d3d12.RENDER_TARGET_BLEND_DESC {
            BlendEnable = false,
            LogicOpEnable = false,

            SrcBlend = .ONE,
            DestBlend = .ZERO,
            BlendOp = .ADD,

            SrcBlendAlpha = .ONE,
            DestBlendAlpha = .ZERO,
            BlendOpAlpha = .ADD,

            LogicOp = .NOOP,
            RenderTargetWriteMask = u8(d3d12.COLOR_WRITE_ENABLE_ALL),
        }

        pipeline_state_desc := d3d12.GRAPHICS_PIPELINE_STATE_DESC {
            pRootSignature = root_signature,
            VS = {
                pShaderBytecode = vs->GetBufferPointer(),
                BytecodeLength = vs->GetBufferSize(),
            },
            PS = {
                pShaderBytecode = ps->GetBufferPointer(),
                BytecodeLength = ps->GetBufferSize(),
            },
            StreamOutput = {},
            BlendState = {
                AlphaToCoverageEnable = false,
                IndependentBlendEnable = false,
                RenderTarget = { 0 = default_blend_state, 1..<7 = {} },
            },
            SampleMask = 0xFFFFFFFF,
            RasterizerState = {
                FillMode = .SOLID,
                CullMode = .BACK,
                FrontCounterClockwise = false,
                DepthBias = 0,
                DepthBiasClamp = 0,
                SlopeScaledDepthBias = 0,
                DepthClipEnable = true,
                MultisampleEnable = false,
                AntialiasedLineEnable = false,
                ForcedSampleCount = 0,
                ConservativeRaster = .OFF,
            },
            DepthStencilState = {
                DepthEnable = false,
                StencilEnable = false,
            },
            InputLayout = {
                pInputElementDescs = &vertex_format[0],
                NumElements = u32(len(vertex_format)),
            },
            PrimitiveTopologyType = .TRIANGLE,
            NumRenderTargets = 1,
            RTVFormats = { 0 = .R8G8B8A8_UNORM, 1..<7 = .UNKNOWN },
            DSVFormat = .UNKNOWN,
            SampleDesc = {
                Count = 1,
                Quality = 0,
            },
        }
        
        hr = device->CreateGraphicsPipelineState(&pipeline_state_desc, d3d12.IPipelineState_UUID, (^rawptr)(&pipeline))
        check(hr, "Pipeline creation failed")

        vs->Release()
        ps->Release()
    }

    // Create the commandlist that is reused further down.
    cmdlist: ^d3d12.IGraphicsCommandList
    hr = device->CreateCommandList(0, .DIRECT, command_allocator, pipeline, d3d12.ICommandList_UUID, (^rawptr)(&cmdlist))
    check(hr, "Failed to create command list")
    hr = cmdlist->Close()
    check(hr, "Failed to close command list")

    vertex_buffer: ^d3d12.IResource
    vertex_buffer_view: d3d12.VERTEX_BUFFER_VIEW

    {
        // The position and color data for the triangle's vertices go together per-vertex
        vertices := [?]f32 {
            // pos            color
             0.0 , 0.5, 0.0,  1,0,0,0,
             0.5, -0.5, 0.0,  0,1,0,0,
            -0.5, -0.5, 0.0,  0,0,1,0,
        }

        heap_props := d3d12.HEAP_PROPERTIES {
            Type = .UPLOAD,
        }

        vertex_buffer_size := len(vertices) * size_of(vertices[0])

        resource_desc := d3d12.RESOURCE_DESC {
            Dimension = .BUFFER,
            Alignment = 0,
            Width = u64(vertex_buffer_size),
            Height = 1,
            DepthOrArraySize = 1,
            MipLevels = 1,
            Format = .UNKNOWN,
            SampleDesc = { Count = 1, Quality = 0 },
            Layout = .ROW_MAJOR,
            Flags = {},
        }

        hr = device->CreateCommittedResource(&heap_props, {}, &resource_desc, d3d12.RESOURCE_STATE_GENERIC_READ, nil, d3d12.IResource_UUID, (^rawptr)(&vertex_buffer))
        check(hr, "Failed creating vertex buffer")

        gpu_data: rawptr
        read_range: d3d12.RANGE

        hr = vertex_buffer->Map(0, &read_range, &gpu_data)
        check(hr, "Failed creating verex buffer resource")

        mem.copy(gpu_data, &vertices[0], vertex_buffer_size)
        vertex_buffer->Unmap(0, nil)

        vertex_buffer_view = d3d12.VERTEX_BUFFER_VIEW {
            BufferLocation = vertex_buffer->GetGPUVirtualAddress(),
            StrideInBytes = u32(vertex_buffer_size/3),
            SizeInBytes = u32(vertex_buffer_size),
        }
    }

    // This fence is used to wait for frames to finish
    fence_value: u64
    fence: ^d3d12.IFence
    fence_event: windows.HANDLE

    {
        hr = device->CreateFence(fence_value, {}, d3d12.IFence_UUID, (^rawptr)(&fence))
        check(hr, "Failed to create fence")
        fence_value += 1
        manual_reset: windows.BOOL = false
        initial_state: windows.BOOL = false
        fence_event = windows.CreateEventW(nil, manual_reset, initial_state, nil)
        if fence_event == nil {
            fmt.println("Failed to create fence event")
            return
        }
    }

    main_loop: for {
        for e: sdl.Event; sdl.PollEvent(&e); {
            #partial switch e.type {
                case .QUIT:
                    break main_loop
                case .WINDOWEVENT:
                    // This is equivalent to WM_PAINT in win32 API
                    if e.window.event == .EXPOSED {
                        hr = command_allocator->Reset()
                        check(hr, "Failed resetting command allocator")

                        hr = cmdlist->Reset(command_allocator, pipeline)
                        check(hr, "Failed to reset command list")

                        viewport := d3d12.VIEWPORT {
                            Width = f32(wx),
                            Height = f32(wy),
                        }

                        scissor_rect := d3d12.RECT {
                            left = 0, right = wx,
                            top = 0, bottom = wy,
                        }

                        // This state is reset everytime the cmd list is reset, so we need to rebind it
                        cmdlist->SetGraphicsRootSignature(root_signature)
                        cmdlist->RSSetViewports(1, &viewport)
                        cmdlist->RSSetScissorRects(1, &scissor_rect)

                        to_render_target_barrier := d3d12.RESOURCE_BARRIER {
                            Type = .TRANSITION,
                            Flags = {},
                        }

                        to_render_target_barrier.Transition = {
                            pResource = targets[frame_index],
                            StateBefore = d3d12.RESOURCE_STATE_PRESENT,
                            StateAfter = {.RENDER_TARGET},
                            Subresource = d3d12.RESOURCE_BARRIER_ALL_SUBRESOURCES,
                        }

                        cmdlist->ResourceBarrier(1, &to_render_target_barrier)

                        rtv_handle: d3d12.CPU_DESCRIPTOR_HANDLE
                        rtv_descriptor_heap->GetCPUDescriptorHandleForHeapStart(&rtv_handle)

                        if (frame_index > 0) {
                            s := device->GetDescriptorHandleIncrementSize(.RTV)
                            rtv_handle.ptr += uint(frame_index * s)
                        }

                        cmdlist->OMSetRenderTargets(1, &rtv_handle, false, nil)

                        // clear backbuffer
                        clearcolor := [?]f32 { 0.05, 0.05, 0.05, 1.0 }
                        cmdlist->ClearRenderTargetView(rtv_handle, &clearcolor, 0, nil)

                        // draw call
                        cmdlist->IASetPrimitiveTopology(.TRIANGLELIST)
                        cmdlist->IASetVertexBuffers(0, 1, &vertex_buffer_view)
                        cmdlist->DrawInstanced(3, 1, 0, 0)
                        
                        to_present_barrier := to_render_target_barrier
                        to_present_barrier.Transition.StateBefore = {.RENDER_TARGET}
                        to_present_barrier.Transition.StateAfter = d3d12.RESOURCE_STATE_PRESENT

                        cmdlist->ResourceBarrier(1, &to_present_barrier)

                        hr = cmdlist->Close()
                        check(hr, "Failed to close command list")

                        // execute
                        cmdlists := [?]^d3d12.IGraphicsCommandList { cmdlist }
                        queue->ExecuteCommandLists(len(cmdlists), (^^d3d12.ICommandList)(&cmdlists[0]))

                        // present
                        {
                            flags: dxgi.PRESENT
                            params: dxgi.PRESENT_PARAMETERS
                            hr = swapchain->Present1(1, flags, &params)
                            check(hr, "Present failed")
                        }

                        // wait for frame to finish
                        {
                            current_fence_value := fence_value

                            hr = queue->Signal(fence, current_fence_value)
                            check(hr, "Failed to signal fence")

                            fence_value += 1
                            completed := fence->GetCompletedValue()

                            if completed < current_fence_value {
                                hr = fence->SetEventOnCompletion(current_fence_value, fence_event)
                                check(hr, "Failed to set event on completion flag")
                                windows.WaitForSingleObject(fence_event, windows.INFINITE)
                            }

                            frame_index = swapchain->GetCurrentBackBufferIndex()
                        }
                    }
            }
        }
    }
}

@thezjy
Copy link

thezjy commented Jan 6, 2025

@rupakhetibinit Strangely when running this code on Windows 11 with odin version dev-2025-01:0cc1dbb09, it fails with exit code 0xC0000135, which means some DLL is missing?

Update: it's the SDL2.dll, I often forget that you need to copy it to the same folder with the exe

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment