Skip to content

Instantly share code, notes, and snippets.

@philipturner
Created June 4, 2025 21:17
Show Gist options
  • Save philipturner/ec3138aaf69d44a46e610a4a0a7a6af2 to your computer and use it in GitHub Desktop.
Save philipturner/ec3138aaf69d44a46e610a4a0a7a6af2 to your computer and use it in GitHub Desktop.
Fifth set of files saved for easy reference, while cleaning up an iteration of the Windows port of Molecular Renderer
// Next steps:
// - Access the GPU.
// - Modify it to get Metal rendering. [DONE]
// - Clean up and simplify the code as much as possible. [DONE]
// - Get timestamps synchronizing properly (moving rainbow banner
// scene). [DONE]
// - Repeat the same process with COM / D3D12 on Windows.
// - Get some general experience with C++ DirectX sample code.
// - Modify the files one-by-one to support Windows.
import MolecularRenderer
#if os(macOS)
import Metal
@MainActor
func createApplication() -> Application {
// Set up the display.
var displayDesc = DisplayDescriptor()
displayDesc.renderTargetSize = 1920
displayDesc.screenID = Display.fastestScreenID
let display = Display(descriptor: displayDesc)
// Set up the GPU context.
var gpuContextDesc = GPUContextDescriptor()
gpuContextDesc.deviceID = GPUContext.fastestDeviceID
let gpuContext = GPUContext(descriptor: gpuContextDesc)
// Set up the application.
var applicationDesc = ApplicationDescriptor()
applicationDesc.display = display
applicationDesc.gpuContext = gpuContext
let application = Application(descriptor: applicationDesc)
return application
}
func createShaderSource() -> String {
"""
#include <metal_stdlib>
using namespace metal;
half convertToChannel(
half hue,
half saturation,
half lightness,
ushort n
) {
half k = half(n) + hue / 30;
k -= 12 * floor(k / 12);
half a = saturation;
a *= min(lightness, 1 - lightness);
half output = min(k - 3, 9 - k);
output = max(output, half(-1));
output = min(output, half(1));
output = lightness - a * output;
return output;
}
kernel void renderImage(
constant float *time0 [[buffer(0)]],
constant float *time1 [[buffer(1)]],
constant float *time2 [[buffer(2)]],
texture2d<half, access::write> drawableTexture [[texture(0)]],
ushort2 tid [[thread_position_in_grid]]
) {
half4 color;
if (tid.y < 1600) {
color = half4(0.707, 0.707, 0.00, 1.00);
} else {
float progress = float(tid.x) / 1920;
if (tid.y < 1600 + 107) {
progress += *time0;
} else if (tid.y < 1600 + 213) {
progress += *time1;
} else {
progress += *time2;
}
half hue = half(progress) * 360;
half saturation = 1.0;
half lightness = 0.5;
half red = convertToChannel(hue, saturation, lightness, 0);
half green = convertToChannel(hue, saturation, lightness, 8);
half blue = convertToChannel(hue, saturation, lightness, 4);
color = half4(red, green, blue, 1.00);
}
drawableTexture.write(color, tid);
}
"""
}
func createRenderPipeline(
application: Application,
shaderSource: String
) -> MTLComputePipelineState {
let device = application.gpuContext.device
let shaderSource = createShaderSource()
let library = try! device.makeLibrary(source: shaderSource, options: nil)
let function = library.makeFunction(name: "renderImage")
guard let function else {
fatalError("Could not make function.")
}
let pipeline = try! device.makeComputePipelineState(function: function)
return pipeline
}
// Set up the resources.
let application = createApplication()
let shaderSource = createShaderSource()
let renderPipeline = createRenderPipeline(
application: application,
shaderSource: shaderSource)
var startTime: UInt64?
var frameID: Int = .zero
// Enter the run loop.
application.run { renderTarget in
frameID += 1
// Start the command encoder.
let commandQueue = application.gpuContext.commandQueue
let commandBuffer = commandQueue.makeCommandBuffer()!
let encoder = commandBuffer.makeComputeCommandEncoder()!
// Bind the buffers.
do {
func setTime(_ time: Double, index: Int) {
let fractionalTime = time - floor(time)
var time32 = Float(fractionalTime)
encoder.setBytes(&time32, length: 4, index: index)
}
if let startTime {
let currentTime = mach_continuous_time()
let timeSeconds = Double(currentTime - startTime) / 24_000_000
setTime(timeSeconds, index: 0)
} else {
startTime = mach_continuous_time()
setTime(Double.zero, index: 0)
}
let clock = application.clock
let timeInFrames = clock.frames
let framesPerSecond = application.display.frameRate
let timeInSeconds = Double(timeInFrames) / Double(framesPerSecond)
setTime(timeInSeconds, index: 1)
setTime(Double.zero, index: 2)
}
// Bind the textures.
encoder.setTexture(renderTarget, index: 0)
// Dispatch
do {
encoder.setComputePipelineState(renderPipeline)
let width = Int(renderTarget.width)
let height = Int(renderTarget.height)
encoder.dispatchThreads(
MTLSize(width: width, height: height, depth: 1),
threadsPerThreadgroup: MTLSize(width: 8, height: 8, depth: 1))
}
// End the command encoder.
encoder.endEncoding()
commandBuffer.commit()
}
#endif
#if os(Windows)
import SwiftCOM
import WinSDK
// The "hello world" compute demo works! Next, render an image to the screen
// using only compute shaders.
//
// First research question: can you create a texture that's backed by a buffer?
// Is the drawable for rendering backed by a buffer? If not, each texture
// should own a unique descriptor table, encapsulated in the utility 'Texture'.
// Notes from the 3DGEP tutorial #4
//
// Texture2D<float4> SrcMip : register(t0);
// RWTexture2D<float4> OutMip1 : register(u0);
//
// "DescriptorTable(SRV(t0, numDescriptors = 1)), " \
// "DescriptorTable(UAV(u0, numDescriptors = 4)), " \
//
// float2 UV;
// Src1 = SrcMip.SampleLevel(LinearClampSampler, UV, SrcMipLevel);
// OutMip1[DispatchThreadID.xy] = PackColor(Src1);
//
// D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {};
// uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
// uavDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
// uavDesc.Texture2D.MipSlice = i;
// uavDesc.Texture2D.PlaneSlice = 0;
//
// device->CreateUnorderedAccessView(
// nullptr, nullptr, &uavDesc,
// m_DefaultUAV.GetDescriptorHandle(i));
//
// There is no actual backing resource for the UAV, so the resource and the
// counter resource are specified as 'nullptr'.
//
// ID3D12Device::CreateShaderResourceView and
// ID3D12Device::CreateUnorderedAccessView only relate to the case where
// resources are stored indirectly as descriptors in tables?
//
// Resources that will be used as a UAV must be created with
// 'UNORDERED_ACCESS' and may not be 'RENDER_TARGET'. Does that mean a
// drawable texture on windows is completely incompatible with writing from a
// compute shader? That sounds like a silly restriction.
//
// It might be possible to copy between textures with a copy command.
//
// The tutorial created textures in a heap. I won't need to do that, because
// the DXGI API supplies me with drawable textures.
//
// I'll have to implement triple buffering somehow. Perhaps with a fence that
// *isn't* the fence used internally during 'CommandQueue.flush()'.
// I'll have to come back to this another day, with a fresh mindset, to make
// more progress.
//
// Let's understand the swapchain before exploring whether textures can be
// backed with buffers. I feel more comfortable with exploring this order of
// priorities.
// Notes on the 1st 3DGEP tutorial:
//
// DXGI 1.6 adds functionality in order to detect HDR displays.
//
// 'chrono::high_resolution_clock' is used to perform timing in between calls
// to the 'Update' function.
//
// 'Vsync' is referenced in the file 'Window.h' from the tutorial's repository.
//
// The swap chain uses 3 back buffers.
//
// Windows Advanced Rasterization Platform is not used.
//
// Notable DirectX API objects:
// - ID3D12Device2
// - IDXGISwapChain4
// - ID3D12Resource g_BackBuffers[3]
// - ID3D12DescriptorHeap
//
// Although the back buffers of the swap chain are actually textures, all
// buffer and texture resources are referenced using the 'ID3D12Resource'
// interface in DirectX 12.
//
// The tutorial uses RTVs to clear the back buffers of the render target. The
// RTVs are created in descriptor heaps, and they describe instances of
// 'ID3D12Resource' that reside in GPU memory.
//
// A view in DirectX 12 is also called a descriptor. One descriptor is needed
// to describe each back buffer texture. The RTVs for the back buffers are
// stored in a descriptor heap.
//
// You must query the size of a descriptor in a descriptor heap. It may vary
// depending on the vendor.
//
// The index of the current back buffer in the swap chain may not be
// sequential (???).
//
// Method of GPU synchronization:
// - ID3D12Fence fence
// - uint64_t fenceValue
// - The next fence value to signal the command queue.
// - uint64_t frameFenceValues[3]
// - Keeps track of the fence values that were used to signal the command
// queue for a particular frame. Guarantees that any resources still being
// referenced by the command queue are not overwritten.
// - HANDLE fenceEvent
//
// VSync and tearing can be toggled. Use windowed instead of fullscreen mode.
//
// By default, the swap chain's present method will block (???) until the next
// vertical refresh of the screen.
//
// Some displays support 'variable refresh rates', which has implications for
// Vsync.
//
// A callback function is used to register the window class.
// Next region of the tutorial: description of the OS windowing API.
//
// Do not create an icon (HICON) for my application. Leave it as the OS default
// icon, all the way through production. I will create an application that
// generalizes beyond the myriad permutations for company names and company
// logos.
//
// There is a cursor class specified in WNDCLASSEXW. For the foreseeable future,
// I prefer to avoid any GUI functionality besides Ctrl+W to close the window.
// The current macOS implementation does not reference mouse events.
//
// The tutorial makes an effort to measure the screen dimensions and center the
// window. The macOS code does this as well. I don't know if Windows has the
// issue of falsifying screen dimensions to satisfy OS text scale factors. My
// PC uses 150%.
//
// A good starting point is to work with the OS-specific APIs for querying
// screen properties.
//
// I don't know what a 'class atom' is.
//
// 'CW_USEDEFAULT' is mentioned multiple times. I do not know how important
// it is.
//
// A window is first created. Then the DirectX resources are created. Finally,
// the window is shown.
// Next region of the tutorial: creation of DXGI resources.
//
// 'DXGI_CREATE_FACTORY_DEBUG' should supposedly be omitted in 'production
// builds'.
//
// It seems that both WARP and non-WARP adapters apply equally to
// 'IDXGIAdapter1' and 'IDXGIAdapter4'.
//
// The tutorial uses the heuristic of selecting the GPU with the largest memory,
// just like my code. Generally speaking, the GPU with the largest amount of
// dedicated video memory is a good indicator of GPU performance. Perhaps
// integrated GPUs have access to significant memory, but it isn't
// "dedicated memory".
//
// Variable refresh-rate displays require tearing (vsync-off) for an app to
// function correctly. I am testing on a fixed refresh-rate display. Tearing
// support was introduced in DXGI 1.5. The tutorial queries whether a computer
// supports tearing. I will not add any explicit support for variable refresh
// rate displays on Windows.
//
// 'IDXGISwapChain' exists, and it has an instance member, 'Present'.
//
// Upon 'Present', the swap chain increments everything in a ring buffer of
// pointers.
//
// 'FLIP_SEQUENTIAL' looks simpler. Presentation lag shouldn't exist if buffers
// are properly guarded with a 3-frame semaphore? This may need to be rigorously
// tested. Or perhaps latency heuristics guarantee it won't cause problems. The
// tutorial uses 'FLIP_DISCARD'.
//
// From the Microsoft docs, 'FLIP_DISCARD' may permit certain optimizations in
// the driver that reduce the amount of copying. These optimizations apply when
// the app is not the only window on the screen (not in fullscreen mode).
//
// The tutorial appears to initialize the 'IDXGIFactory4' multiple times. These
// initializations are redundant, but could be important for encapsulating code.
//
// The back buffer's pixel format is specified in 'DXGI_SWAP_CHAIN_DESC1'.
// 'DXGI_SWAP_CHAIN_DESC' also allows the pixel format to be specified. And it
// is the only one that lets you specify the refresh rate. However, this one
// has been deprecated since DirectX 11.1.
//
// Another thing to note: on Mac, I can test setups with multiple displays,
// and automatically choose the one with the fastest refresh rate. On Windows,
// I cannot test such a feature.
//
// 'IDXGIFactory2::CreateSwapChainForHwnd' requires the swap chain descriptor
// to be 'DXGI_SWAP_CHAIN_DESC1'.
//
// 'ALT' + 'ENTER' can force a window to fullscreen. I don't want that for my
// use case. There is a way to prevent that from happening.
//
// The tutorial uses 'ClearRenderTargetView', but one can probably get away
// with 'ClearUnorderedAccessViewXxx'. It's still not clear whether the back
// buffer's resource can be set to a buffer instead of a texture.
//
// 'Present' should use a sync interval of 1.
//
// The tutorial issues a 'Signal' between 'ExecuteCommandLists' and 'Present'.
// My existing helper class makes this use case impossible. Except... the fence
// inside this utility is not the fence for triple-buffer semaphores. So it is
// not a concern.
// Window Message Procedure
//
// Events:
// - Repaint a portion of the window's contents.
// - Respond to key presses when the window is in focus.
// - Respond to resize events (which shouldn't happen for my application).
//
// Not fixing the annoying sound in response to SYSCHAR. But what is the sound?
// It's the standard Windows error chime.
//
// It is important to respond to WM_DESTROY. Call 'PostQuitMessage(0)', which
// terminates the current process. It may be similar to 'exit(0)' on macOS.
// Both 'PostQuitMessage' and 'exit' exist on Windows, but the former doesn't
// actually terminate the application. It looks reasonable to just call
// 'exit(0)' on Windows.
//
// I wonder what happens if I don't call 'DefWindowProc' for messages that
// aren't relevant. There should be nothing wrong with skipping this function
// call.
// SetThreadDpiAwarenessContext
//
// The documentation mentions 'DPI_AWARENESS_CONTEXT' and 'DPI_AWARENESS'. But
// it seems that only 'DPI_AWARENESS_CONTEXT' has any relevance.
//
// What is the old 'dpiContext' returned on my computer?
// existing 0x0000000080006010
// UNAWARE 0x0000000000006010
// SYSTEM_AWARE 0x0000000000009011
// PER_MONITOR_AWARE 0x0000000000000012
// PER_MONITOR_AWARE_V2 0x0000000000000022
// UNAWARE_GDISCALED 0x0000000040006010
//
// The pointer passed out does not equal the pointer entered in. It doesn't
// even correspond to the input pointer arithmetically. I would expect it to be
// UInt64.max - (input pointer).
//
// DPI_AWARENESS_CONTEXT = UnsafeMutablePointer<DPI_AWARENESS_CONTEXT__>
// DPI_AWARENESS_CONTEXT = struct type, has property 'unused' of type Int32
// DPI_AWARENESS = enumeration, has raw value of type Int32
// Tomorrow, with a fresh mindset, I can do something about this. Start by
// inspecting the functions that report screen properties.
//
// Screen dimensions with different awareness contexts:
// existing 2560x1440
// UNAWARE 2560x1440
// SYSTEM_AWARE 3840x2160
// PER_MONITOR_AWARE 3840x2160
// PER_MONITOR_AWARE_V2 3840x2160
// UNAWARE_GDISCALED 2560x1440
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
// TODO: Set the Window name on macOS to match the one on Windows? Or reserve
// that GUI decision to one that generalizes to custom UIs. For now, just copy
// the names from the 3DGEP tutorial.
//
// I learned something interesting. Some OS functions have two variants, one
// suffixed with "A" and the other suffixed with "ExW/EXW". The former employs
// strings with 8-bit characters. The latter employs strings with 16-bit
// characters.
//
// Wait...there might be distinct concerns here:
// - "A" vs "W"
// - "Ex" vs not "Ex"
// - 32-bit vs 64-bit operating systems
//
// typedef struct tagWNDCLASSA {
// UINT style;
// WNDPROC lpfnWndProc;
// int cbClsExtra;
// int cbWndExtra;
// HINSTANCE hInstance;
// HICON hIcon;
// HCURSOR hCursor;
// HBRUSH hbrBackground;
// LPCSTR lpszMenuName;
// LPCSTR lpszClassName;
// } WNDCLASSA, *PWNDCLASSA, *NPWNDCLASSA, *LPWNDCLASSA;
//
// typedef struct tagWNDCLASSEXA {
// UINT cbSize;
// UINT style;
// WNDPROC lpfnWndProc;
// int cbClsExtra;
// int cbWndExtra;
// HINSTANCE hInstance;
// HICON hIcon;
// HCURSOR hCursor;
// HBRUSH hbrBackground;
// LPCSTR lpszMenuName;
// LPCSTR lpszClassName;
// HICON hIconSm;
// } WNDCLASSEXA, *PWNDCLASSEXA, *NPWNDCLASSEXA, *LPWNDCLASSEXA;
//
// The "ATOM" return type is a 16-bit integer. The user can't do anything with
// the value, except check that it's not 0.
//
// HWND CreateWindowA(
// [in, optional] LPCSTR lpClassName,
// [in, optional] LPCSTR lpWindowName,
// [in] DWORD dwStyle,
// [in] int x,
// [in] int y,
// [in] int nWidth,
// [in] int nHeight,
// [in, optional] HWND hWndParent,
// [in, optional] HMENU hMenu,
// [in, optional] HINSTANCE hInstance,
// [in, optional] LPVOID lpParam
// );
//
// HWND CreateWindowExA(
// [in] DWORD dwExStyle,
// [in, optional] LPCSTR lpClassName,
// [in, optional] LPCSTR lpWindowName,
// [in] DWORD dwStyle,
// [in] int X,
// [in] int Y,
// [in] int nWidth,
// [in] int nHeight,
// [in, optional] HWND hWndParent,
// [in, optional] HMENU hMenu,
// [in, optional] HINSTANCE hInstance,
// [in, optional] LPVOID lpParam
// );
// Choices for 'WNDCLASS':
//
// Use 'WNDCLASSEX' instead of 'WNDCLASS'.
// Use 'A' instead of 'W'.
//
// cbSize = sizeof(WNDCLASSEXA)
// style = 0
// lpfnWndProc = TODO
// hInstance = TODO
// hIcon = nullptr
// hCursor = LoadCursor(nullptr, IDC_ARROW)
// hbrBackground = HBRUSH(bitPattern: Int(COLOR_WINDOW + 1))
// lpszClassName = "DX12WindowClass"
// hSmIcon = nullptr
//
// In the default initializer for the struct, everything is initialized to 0.
// The code can be made shorter by just not mentioning these members.
//
// There is an issue with the HINSTANCE. When I use the instance from the
// Workspace executable, it will be different than when the function is
// encapsulated in a library. Try setting 'hInstance' to 'nullptr' for now.
//
// #define MAKEINTRESOURCEA(i) ((LPSTR)((ULONG_PTR)((WORD)(i))))
// #define IDC_ARROW MAKEINTRESOURCE(32512)
func messageProcedure(
hwnd: HWND?,
message: UInt32,
wParam: WPARAM,
lParam: LPARAM
) -> LRESULT {
print("Called the message procedure with message code \(message).")
switch message {
case UInt32(WM_PAINT): print("Called WM_PAINT")
case UInt32(WM_SYSKEYDOWN): print("Called WM_SYSKEYDOWN")
case UInt32(WM_KEYDOWN): print("Called WM_KEYDOWN")
case UInt32(WM_SYSCHAR): print("Called WM_SYSCHAR")
case UInt32(WM_SIZE): print("Called WM_SIZE")
case UInt32(WM_DESTROY): print("Called WM_DESTROY")
default: print("Called unknown message")
}
// Defer to the OS default function.
return DefWindowProcA(hwnd, message, wParam, lParam)
}
// WARNING: Captures 'messageProcedure' from the outer scope. Encapsulate this
// better when you migrate this to the helper library.
//
// Leaving all of the null variables explicitly initialized, to ease the pain
// of explicitly checking them when tracing down a bug.
func registerWindowClass(name: String) {
// Specify the first few parameters of the window class descriptor.
var windowClass = WNDCLASSEXA()
windowClass.cbSize = UInt32(MemoryLayout<WNDCLASSEXA>.stride)
windowClass.style = UInt32(CS_HREDRAW | CS_VREDRAW)
windowClass.lpfnWndProc = messageProcedure
windowClass.cbClsExtra = 0
windowClass.cbWndExtra = 0
windowClass.hInstance = nil
// Generate the cursor object.
let cursorName = UnsafeMutablePointer<Int8>(bitPattern: UInt(32512))
let cursor = LoadCursorA(nil, cursorName)
windowClass.hCursor = cursor
windowClass.hbrBackground = HBRUSH(bitPattern: Int(COLOR_WINDOW + 1))
// Set the icon properties.
let iconName = UnsafeMutablePointer<Int8>(bitPattern: UInt(32512))
let icon = LoadIconA(nil, iconName)
windowClass.hIcon = icon
windowClass.hIconSm = icon
// 'RegisterClassExA' must be called within the same scope where the cString
// pointer exists. Otherwise, cString becomes a zombie pointer and the
// function fails with error code 123.
name.withCString { cString in
windowClass.lpszMenuName = nil
windowClass.lpszClassName = cString
let atom = RegisterClassExA(&windowClass)
guard atom > 0 else {
let errorCode = GetLastError()
fatalError(
"Could not create window class. Received error code \(errorCode).")
}
}
}
registerWindowClass(name: "DX12WindowClass")
// This worked. Next, create the window.
//
// OVERLAPPEDWINDOW is a combination of styles.
// - OVERLAPPED: top-level window, application's main window
// - CAPTION: has a title bar
// - SYSMENU: perhaps something only visible upon pressing ALT + SPACE? I don't
// want 'File', 'Edit', etc. to show. If this is a pop-up window, hopefully
// it is a temporary window.
// - THICKFRAME: has a sizing border. I want to eliminate this, because the
// user should not be able to resize the window. Perhaps keep the border for
// now, to identify all possible sources of resizing events. A window must
// have CAPTION or THICKFRAME to receive a WM_GETMINMAXINFO message.
// - WS_MINIMIZEBOX: the title bar has a minimize button. This option can only
// be specified if SYSMENU is also specified.
// - WS_MAXIMIZEBOX: the title bar has a maximize button. This option can only
// be specified if SYSMENU is also specified. I don't want the user to be
// able to enlarge the window to fullscreen / near-fullscreen. Keep this
// option available and diagnose it as a source of resize events. Also, try
// to keep consistency with the window structure on macOS.
//
// dwExStyle = 0
// windowClassName = "DX12WindowClass"
// windowTitle = "Learning DirectX 12"
// dwStyle = WS_OVERLAPPEDWINDOW
// X = defined in other code (TODO)
// Y = defined in other code (TODO)
// windowWidth = defined in other code (TODO)
// windowHeight = defined in other code (TODO)
// hWndParent = NULL
// hMenu = NULL
// hInstance = TODO
// lpParam = nullptr
//
// Interesting note: 'NULL' is not exactly the same as 'nullptr'. NULL is a
// macro that substitutes for the value '0'. It could apply to any integer
// type. Meanwhile, nullptr applies exclusively to pointer types.
// Returns the window size and position.
// Lane 0: x
// Lane 1: y
// Lane 2: width
// Lane 3: height
func createWindowDimensions() -> SIMD4<UInt32> {
// (3840, 2160)
let screenWidth = Int32(GetSystemMetrics(SM_CXSCREEN))
let screenHeight = Int32(GetSystemMetrics(SM_CYSCREEN))
// (0, 0, 1440, 1440) -> (-11, -45, 1451, 1451)
var windowRect = RECT()
windowRect.left = 0
windowRect.top = 0
windowRect.right = 1440
windowRect.bottom = 1440
AdjustWindowRect(&windowRect, WS_OVERLAPPEDWINDOW, false)
// (1462, 1496)
let windowSizeX = Int32(windowRect.right - windowRect.left)
let windowSizeY = Int32(windowRect.bottom - windowRect.top)
// (1920, 1080)
let centerX = screenWidth / 2
let centerY = screenHeight / 2
// (1189, 332)
let leftX = centerX - windowSizeX / 2
let upperY = centerY - windowSizeY / 2
// Not clamping because we don't do this on Mac either. Instead, crashing if
// we detect an out-of-bounds error. May remove this check in the future. It
// feels fair to also check if the bottom right corner is out of bounds, but
// that goes beyond the spirit of the 3DGEP tutorial.
guard leftX >= 0,
upperY >= 0 else {
fatalError("Window origin was out of bounds.")
}
let outputSigned = SIMD4<Int32>(
leftX, upperY, windowSizeX, windowSizeY)
let outputUnsigned = SIMD4<UInt32>(
truncatingIfNeeded: outputSigned)
return outputUnsigned
}
struct WindowDescriptor {
var className: String?
var title: String?
var dimensions: SIMD4<UInt32>?
}
func createWindow(descriptor: WindowDescriptor) -> HWND {
guard let className = descriptor.className,
let title = descriptor.title,
let dimensions = descriptor.dimensions else {
fatalError("Descriptor was incomplete.")
}
let output = CreateWindowExA(
0, // dwExStyle
className, // lpClassName
title, // lpWindowName
WS_OVERLAPPEDWINDOW, // dwStyle
Int32(dimensions[0]), // X
Int32(dimensions[1]), // Y
Int32(dimensions[2]), // nWidth
Int32(dimensions[3]), // nHeight
nil, // hWndParent
nil, // hMenu
nil, // hInstance
nil) // lpParam
guard let output else {
let errorCode = GetLastError()
fatalError(
"Failed to create window. Received error code \(errorCode).")
}
return output
}
// Test the window creation procedure.
var windowDesc = WindowDescriptor()
windowDesc.className = "DX12WindowClass"
windowDesc.title = "Learning DirectX 12"
windowDesc.dimensions = createWindowDimensions()
let window = createWindow(descriptor: windowDesc)
// I think the next task is setting up the swap chain.
struct SwapChainDescriptor {
var commandQueue: CommandQueue?
var window: HWND?
}
func createSwapChain(
descriptor: SwapChainDescriptor
) -> SwiftCOM.IDXGISwapChain4 {
guard let commandQueue = descriptor.commandQueue,
let window = descriptor.window else {
fatalError("Descriptor was incomplete.")
}
// Instantiate the factory.
let factory: SwiftCOM.IDXGIFactory4 =
try! CreateDXGIFactory2(UInt32(DXGI_CREATE_FACTORY_DEBUG))
// Fill the swap chain descriptor.
var swapChainDesc = DXGI_SWAP_CHAIN_DESC1()
swapChainDesc.Width = 1440
swapChainDesc.Height = 1440
swapChainDesc.Format = DXGI_FORMAT_R10G10B10A2_UNORM
swapChainDesc.Stereo = false
// Specify the multisampling descriptor.
var sampleDesc = DXGI_SAMPLE_DESC()
sampleDesc.Count = 1
sampleDesc.Quality = 0
swapChainDesc.SampleDesc = sampleDesc
// Compute-centric workflow: write to a custom UAV resource in the shader,
// copy to the back buffer with 'ID3D12GraphicsCommandList::CopyResource'.
//
// https://stackoverflow.com/a/78501260
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT
swapChainDesc.BufferCount = 3
swapChainDesc.Scaling = DXGI_SCALING_NONE
// I'm choosing flip discard, although I'm still troubled over whether this
// is the option I want.
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD
// I'm also troubled over the best option for the alpha mode.
swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED
swapChainDesc.Flags = 0
// Get the swap chain as 'IDXGISwapChain1'.
var swapChain1: SwiftCOM.IDXGISwapChain1
swapChain1 = try! factory.CreateSwapChainForHwnd(
commandQueue.d3d12CommandQueue, // pDevice
window, // hWnd
swapChainDesc, // pDesc
nil, // pFullscreenDesc,
nil) // pRestrictToOutput
// Perform a cast using 'IUnknown::QueryInterface'.
var swapChain4: SwiftCOM.IDXGISwapChain4
swapChain4 = try! swapChain1.QueryInterface()
return swapChain4
}
// List all of the DXGI debug IDs for reference.
let dxgiDebugIDs: [DXGI_DEBUG_ID] = [
//DXGI_DEBUG_ALL,
//DXGI_DEBUG_DX,
DXGI_DEBUG_DXGI,
//DXGI_DEBUG_APP,
//DXGI_DEBUG_D3D11
]
// Create the device.
let device = Device()
let infoQueue = device.d3d12InfoQueue
try! infoQueue.ClearStorageFilter()
// Initialize the DXGI info queue.
var infoQueue2: SwiftCOM.IDXGIInfoQueue
infoQueue2 = try! DXGIGetDebugInterface1(0)
try! infoQueue2.SetBreakOnSeverity(
DXGI_DEBUG_DXGI, DXGI_INFO_QUEUE_MESSAGE_SEVERITY_ERROR, true)
// Create the command queue.
var commandQueueDescriptor = CommandQueueDescriptor()
commandQueueDescriptor.device = device
let commandQueue = CommandQueue(descriptor: commandQueueDescriptor)
// Create the swap chain.
var swapChainDesc = SwapChainDescriptor()
swapChainDesc.commandQueue = commandQueue
swapChainDesc.window = window
let swapChain = createSwapChain(descriptor: swapChainDesc)
#if false
// MARK: - Section 1 of Implemented Methods
print()
print(try! infoQueue.GetRetrievalFilterStackSize())
for dxgiDebugID in dxgiDebugIDs {
print("-", try! infoQueue2.GetRetrievalFilterStackSize(dxgiDebugID))
}
print()
print(try! infoQueue.GetStorageFilterStackSize())
for dxgiDebugID in dxgiDebugIDs {
print("-", try! infoQueue2.GetStorageFilterStackSize(dxgiDebugID))
}
print()
print(try! infoQueue.GetMessageCountLimit())
for dxgiDebugID in dxgiDebugIDs {
print("-", try! infoQueue2.GetMessageCountLimit(dxgiDebugID))
}
print()
print(try! infoQueue.GetMuteDebugOutput())
for dxgiDebugID in dxgiDebugIDs {
print("-", try! infoQueue2.GetMuteDebugOutput(dxgiDebugID))
}
// MARK: - Section 2 of Implemented Methods
print()
print("Start of Section 2")
print()
print(try! infoQueue.GetNumMessagesAllowedByStorageFilter())
for dxgiDebugID in dxgiDebugIDs {
print("-", try! infoQueue2.GetNumMessagesAllowedByStorageFilter(dxgiDebugID))
}
print()
print(try! infoQueue.GetNumMessagesDeniedByStorageFilter())
for dxgiDebugID in dxgiDebugIDs {
print("-", try! infoQueue2.GetNumMessagesDeniedByStorageFilter(dxgiDebugID))
}
print()
print(try! infoQueue.GetNumStoredMessages())
for dxgiDebugID in dxgiDebugIDs {
print("-", try! infoQueue2.GetNumStoredMessages(dxgiDebugID))
}
print()
print(try! infoQueue.GetNumStoredMessagesAllowedByRetrievalFilter())
for dxgiDebugID in dxgiDebugIDs {
print("-", try! infoQueue2.GetNumStoredMessagesAllowedByRetrievalFilters(dxgiDebugID))
}
print()
print("End of Section 2")
// Next task:
// Test whether the info queue catches the errors for IDXGISwapChain.
#endif
#if false
// Show the debug messages.
do {
let messageCount = try! infoQueue.GetNumStoredMessages()
for messageID in 0..<messageCount {
let message =
try! infoQueue.GetMessage(UInt64(messageID))
print("messages[\(messageID)]:")
print("- category:", message.pointee.Category)
print("- severity:", message.pointee.Severity)
print("- ID:", message.pointee.ID)
let description = String(cString: message.pointee.pDescription)
print("- description:", description)
print("- byte length:", message.pointee.DescriptionByteLength)
free(message)
}
}
#endif
#if false
for dxgiDebugID in dxgiDebugIDs {
print("DXGI debug ID: \(dxgiDebugID)")
let messageCount = try! infoQueue2.GetNumStoredMessages(dxgiDebugID)
for messageID in 0..<messageCount {
let message =
try! infoQueue2.GetMessage(dxgiDebugID, UInt64(messageID))
print("- messages[\(messageID)]:")
print(" - category:", message.pointee.Category)
print(" - severity:", message.pointee.Severity)
print(" - ID:", message.pointee.ID)
let description = String(cString: message.pointee.pDescription)
print(" - description:", description)
print(" - byte length:", message.pointee.DescriptionByteLength)
free(message)
}
}
#endif
// Next steps:
// (1) Migrate 'IDXGIInfoQueue' to the fork of swift-com.
// (2) Continue developing the above code as-is, until the tutorial is finished.
// (3) Clear the render target with a compute shader + copy command, instead of
// render commands. This provides a clearer vision of the functionality
// needed in a utility library.
// (4) Archive and purge 'main.swift' and 'VectorAddition.swift'.
// (5) Incorporate code handling 'HWND', 'IDXGISwapChain', and 'IDXGIInfoQueue'
// into the helper library.
// Descriptor heap:
// - NumDescriptors: 3
// - Type: D3D12_DESCRIPTOR_HEAP_TYPE_RTV
func createDescriptorHeap(device: Device) -> SwiftCOM.ID3D12DescriptorHeap {
// Fill the descriptor.
var descriptorHeapDesc = D3D12_DESCRIPTOR_HEAP_DESC()
descriptorHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV
descriptorHeapDesc.NumDescriptors = 3
descriptorHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE
descriptorHeapDesc.NodeMask = 0
// Create the descriptor heap.
let d3d12Device = device.d3d12Device
var descriptorHeap: SwiftCOM.ID3D12DescriptorHeap
descriptorHeap = try! d3d12Device
.CreateDescriptorHeap(descriptorHeapDesc)
return descriptorHeap
}
let descriptorHeap = createDescriptorHeap(device: device)
// Two objectives:
// - Fetch the resource objects for the backing buffers.
// - Write the render target views into the descriptor heap.
//
// Back buffers should be grouped with the swap chain in a utility class.
var backBuffers: [SwiftCOM.ID3D12Resource] = []
for backBufferID in 0..<3 {
// Retrieve the back buffer.
var backBuffer: SwiftCOM.ID3D12Resource
backBuffer = try! swapChain
.GetBuffer(UInt32(backBufferID))
backBuffers.append(backBuffer)
// Compute the address of the CPU descriptor handle.
let rtvDescriptorSize = try! device.d3d12Device
.GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV)
var rtvHandle = try! descriptorHeap
.GetCPUDescriptorHandleForHeapStart()
rtvHandle.ptr += UInt64(backBufferID) * UInt64(rtvDescriptorSize)
// Write to a location in the descriptor heap.
try! device.d3d12Device.CreateRenderTargetView(
backBuffer, // pResource
nil, // pDesc
rtvHandle) // DestDescriptor
}
print("Hello world.")
// Next task: set up the render loop.
//
// This is a precursor to encoding any commands, render or compute.
//
// Need to wrap up several objects into a global "Application" or other state
// tracker. This means some amount of purging. Unsure how far to go regarding
// wrapping code into library utility classes. Best to leave any utilities
// centered around RTV, then transition to compute/UAV workflows when the
// code base is better suited for this.
//
// I could also leave some code in the "Workspace" module when it's not
// developed enough to go into the library.
// Issue with swift-com: IDXGISwapChain doesn't show proper class inheritance.
// Fixing that before proceeding.
let currentBackBufferIndex = try! swapChain.GetCurrentBackBufferIndex()
print(currentBackBufferIndex)
#endif
#if os(Windows)
import MolecularRenderer
/// Utility code for setting up a vector addition test.
class VectorAddition {
let inputBuffer0: Buffer
let inputBuffer1: Buffer
let nativeBuffer0: Buffer
let nativeBuffer1: Buffer
let nativeBuffer2: Buffer
let outputBuffer2: Buffer
init(device: Device) {
// Fill the descriptor properties common to all buffers.
var bufferDesc = BufferDescriptor()
bufferDesc.device = device
bufferDesc.size = 1024 * 4
// Create the input buffers.
bufferDesc.type = .input
self.inputBuffer0 = Buffer(descriptor: bufferDesc)
self.inputBuffer1 = Buffer(descriptor: bufferDesc)
// Create the native buffers.
bufferDesc.type = .native
self.nativeBuffer0 = Buffer(descriptor: bufferDesc)
self.nativeBuffer1 = Buffer(descriptor: bufferDesc)
self.nativeBuffer2 = Buffer(descriptor: bufferDesc)
// Create the output buffers.
bufferDesc.type = .output
self.outputBuffer2 = Buffer(descriptor: bufferDesc)
// Generate the input data for the shader.
var inputData0: [Float] = []
var inputData1: [Float] = []
for i in 0..<1024 {
let value0 = Float(i)
let value1 = 1024 + Float(i)
inputData0.append(value0)
inputData1.append(value1)
}
inputData0.withUnsafeBytes { bufferPointer in
let baseAddress = bufferPointer.baseAddress!
inputBuffer0.write(input: baseAddress)
}
inputData1.withUnsafeBytes { bufferPointer in
let baseAddress = bufferPointer.baseAddress!
inputBuffer1.write(input: baseAddress)
}
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment