diff --git a/tests/RHI/D3D12/integration/CMakeLists.txt b/tests/RHI/D3D12/integration/CMakeLists.txt index 75f4fba2..09b147f5 100644 --- a/tests/RHI/D3D12/integration/CMakeLists.txt +++ b/tests/RHI/D3D12/integration/CMakeLists.txt @@ -97,6 +97,50 @@ add_custom_command(TARGET D3D12_RenderModel POST_BUILD $/ ) +# Triangle test - renders a rainbow gradient triangle +add_executable(D3D12_Triangle + WIN32 + triangle/main.cpp +) + +target_include_directories(D3D12_Triangle PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/triangle + ${ENGINE_ROOT_DIR}/include +) + +target_compile_definitions(D3D12_Triangle PRIVATE + UNICODE + _UNICODE +) + +target_link_libraries(D3D12_Triangle PRIVATE + d3d12 + dxgi + d3dcompiler + winmm + XCEngine +) + +# Copy Res folder to output directory for Triangle test +add_custom_command(TARGET D3D12_Triangle POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/triangle/Res + $/Res +) + +# Copy test scripts to output directory for Triangle test +add_custom_command(TARGET D3D12_Triangle POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/run.bat + $/ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/compare_ppm.py + $/ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/run_integration_test.py + $/ +) + # Register integration tests with CTest add_test(NAME D3D12_Minimal_Integration COMMAND ${Python3_EXECUTABLE} $/run_integration_test.py @@ -115,3 +159,12 @@ add_test(NAME D3D12_RenderModel_Integration 5 WORKING_DIRECTORY $ ) + +add_test(NAME D3D12_Triangle_Integration + COMMAND ${Python3_EXECUTABLE} $/run_integration_test.py + $ + triangle.ppm + ${CMAKE_CURRENT_SOURCE_DIR}/triangle/GT.ppm + 5 + WORKING_DIRECTORY $ +) diff --git a/tests/RHI/D3D12/integration/triangle/GT.ppm b/tests/RHI/D3D12/integration/triangle/GT.ppm new file mode 100644 index 00000000..6fd275e4 Binary files /dev/null and b/tests/RHI/D3D12/integration/triangle/GT.ppm differ diff --git a/tests/RHI/D3D12/integration/triangle/Res/Shader/triangle.hlsl b/tests/RHI/D3D12/integration/triangle/Res/Shader/triangle.hlsl new file mode 100644 index 00000000..8606c24e --- /dev/null +++ b/tests/RHI/D3D12/integration/triangle/Res/Shader/triangle.hlsl @@ -0,0 +1,20 @@ +struct Vertex { + float4 pos : POSITION; + float4 col : COLOR; +}; + +struct VSOut { + float4 pos : SV_POSITION; + float4 col : COLOR; +}; + +VSOut MainVS(Vertex v) { + VSOut o; + o.pos = v.pos; + o.col = v.col; + return o; +} + +float4 MainPS(VSOut i) : SV_TARGET { + return i.col; +} \ No newline at end of file diff --git a/tests/RHI/D3D12/integration/triangle/main.cpp b/tests/RHI/D3D12/integration/triangle/main.cpp new file mode 100644 index 00000000..1b969522 --- /dev/null +++ b/tests/RHI/D3D12/integration/triangle/main.cpp @@ -0,0 +1,360 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "XCEngine/RHI/RHIEnums.h" +#include "XCEngine/RHI/RHITypes.h" +#include "XCEngine/RHI/D3D12/D3D12Device.h" +#include "XCEngine/RHI/D3D12/D3D12CommandQueue.h" +#include "XCEngine/RHI/D3D12/D3D12CommandAllocator.h" +#include "XCEngine/RHI/D3D12/D3D12CommandList.h" +#include "XCEngine/RHI/D3D12/D3D12DescriptorHeap.h" +#include "XCEngine/RHI/D3D12/D3D12Fence.h" +#include "XCEngine/RHI/D3D12/D3D12SwapChain.h" +#include "XCEngine/RHI/D3D12/D3D12Buffer.h" +#include "XCEngine/RHI/D3D12/D3D12Texture.h" +#include "XCEngine/RHI/D3D12/D3D12RenderTargetView.h" +#include "XCEngine/RHI/D3D12/D3D12DepthStencilView.h" +#include "XCEngine/RHI/D3D12/D3D12Shader.h" +#include "XCEngine/RHI/D3D12/D3D12RootSignature.h" +#include "XCEngine/RHI/D3D12/D3D12PipelineState.h" +#include "XCEngine/RHI/D3D12/D3D12Screenshot.h" +#include "XCEngine/Debug/Logger.h" +#include "XCEngine/Debug/ConsoleLogSink.h" +#include "XCEngine/Debug/FileLogSink.h" +#include "XCEngine/Containers/String.h" + +using namespace XCEngine::RHI; +using namespace XCEngine::Debug; +using namespace XCEngine::Containers; + +#pragma comment(lib,"d3d12.lib") +#pragma comment(lib,"dxgi.lib") +#pragma comment(lib,"dxguid.lib") +#pragma comment(lib,"d3dcompiler.lib") +#pragma comment(lib,"winmm.lib") + +D3D12Device gDevice; +D3D12CommandQueue gCommandQueue; +D3D12SwapChain gSwapChain; +D3D12CommandAllocator gCommandAllocator; +D3D12CommandList gCommandList; +D3D12Texture gDepthStencil; +D3D12DescriptorHeap gRTVHeap; +D3D12DescriptorHeap gDSVHeap; +D3D12RenderTargetView gRTVs[2]; +D3D12DepthStencilView gDSV; + +D3D12Shader gVertexShader; +D3D12Shader gPixelShader; +D3D12RootSignature gRootSignature; +D3D12PipelineState gPipelineState; +D3D12Buffer gVertexBuffer; + +UINT gRTVDescriptorSize = 0; +UINT gDSVDescriptorSize = 0; +int gCurrentRTIndex = 0; + +HWND gHWND = nullptr; +int gWidth = 1280; +int gHeight = 720; + +void Log(const char* format, ...) { + char buffer[1024]; + va_list args; + va_start(args, format); + vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + Logger::Get().Debug(LogCategory::Rendering, String(buffer)); +} + +LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + switch (msg) { + case WM_CLOSE: + PostQuitMessage(0); + break; + } + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +bool InitD3D12() { + RHIDeviceDesc deviceDesc; + deviceDesc.windowHandle = gHWND; + deviceDesc.width = gWidth; + deviceDesc.height = gHeight; + deviceDesc.adapterIndex = 0; + deviceDesc.enableDebugLayer = false; + deviceDesc.enableGPUValidation = false; + + if (!gDevice.Initialize(deviceDesc)) { + Log("[ERROR] Failed to initialize D3D12 device"); + return false; + } + + ID3D12Device* device = gDevice.GetDevice(); + IDXGIFactory4* factory = gDevice.GetFactory(); + + if (!gCommandQueue.Initialize(device, CommandQueueType::Direct)) { + Log("[ERROR] Failed to initialize command queue"); + return false; + } + + if (!gSwapChain.Initialize(factory, gCommandQueue.GetCommandQueue(), gHWND, gWidth, gHeight, 2)) { + Log("[ERROR] Failed to initialize swap chain"); + return false; + } + + gDepthStencil.InitializeDepthStencil(device, gWidth, gHeight); + + gRTVHeap.Initialize(device, DescriptorHeapType::RTV, 2); + gRTVDescriptorSize = gDevice.GetDescriptorHandleIncrementSize(DescriptorHeapType::RTV); + + gDSVHeap.Initialize(device, DescriptorHeapType::DSV, 1); + gDSVDescriptorSize = gDevice.GetDescriptorHandleIncrementSize(DescriptorHeapType::DSV); + + for (int i = 0; i < 2; i++) { + D3D12Texture& backBuffer = gSwapChain.GetBackBuffer(i); + CPUDescriptorHandle rtvCpuHandle = gRTVHeap.GetCPUDescriptorHandle(i); + D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = { rtvCpuHandle.ptr }; + gRTVs[i].InitializeAt(device, backBuffer.GetResource(), rtvHandle, nullptr); + } + + D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc = D3D12DepthStencilView::CreateDesc(Format::D24_UNorm_S8_UInt); + CPUDescriptorHandle dsvCpuHandle = gDSVHeap.GetCPUDescriptorHandle(0); + D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = { dsvCpuHandle.ptr }; + gDSV.InitializeAt(device, gDepthStencil.GetResource(), dsvHandle, &dsvDesc); + + gCommandAllocator.Initialize(device, CommandQueueType::Direct); + gCommandList.Initialize(device, CommandQueueType::Direct, gCommandAllocator.GetCommandAllocator()); + + if (!gVertexShader.CompileFromFile(L"Res/Shader/triangle.hlsl", "MainVS", "vs_5_1")) { + Log("[ERROR] Failed to compile vertex shader"); + return false; + } + Log("[INFO] Vertex shader compiled, bytecode size: %zu", gVertexShader.GetBytecodeSize()); + + if (!gPixelShader.CompileFromFile(L"Res/Shader/triangle.hlsl", "MainPS", "ps_5_1")) { + Log("[ERROR] Failed to compile pixel shader"); + return false; + } + Log("[INFO] Pixel shader compiled, bytecode size: %zu", gPixelShader.GetBytecodeSize()); + + D3D12_ROOT_SIGNATURE_DESC rsDesc = D3D12RootSignature::CreateDesc(nullptr, 0); + if (!gRootSignature.Initialize(device, rsDesc)) { + Log("[ERROR] Failed to initialize root signature"); + return false; + } + + D3D12_INPUT_ELEMENT_DESC inputElements[] = { + D3D12PipelineState::CreateInputElement("POSITION", 0, Format::R32G32B32A32_Float, 0, 0), + D3D12PipelineState::CreateInputElement("COLOR", 0, Format::R32G32B32A32_Float, 0, 16), + }; + + D3D12_SHADER_BYTECODE emptyGs = {}; + + D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {}; + psoDesc.pRootSignature = gRootSignature.GetRootSignature(); + psoDesc.VS = gVertexShader.GetD3D12Bytecode(); + psoDesc.PS = gPixelShader.GetD3D12Bytecode(); + psoDesc.GS = emptyGs; + psoDesc.InputLayout.NumElements = 2; + psoDesc.InputLayout.pInputElementDescs = inputElements; + psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; + psoDesc.DSVFormat = DXGI_FORMAT_D24_UNORM_S8_UINT; + psoDesc.SampleDesc.Count = 1; + psoDesc.SampleDesc.Quality = 0; + psoDesc.SampleMask = 0xffffffff; + psoDesc.NumRenderTargets = 1; + psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + psoDesc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; + psoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + psoDesc.RasterizerState.FrontCounterClockwise = FALSE; + psoDesc.RasterizerState.DepthClipEnable = TRUE; + psoDesc.DepthStencilState.DepthEnable = FALSE; + psoDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO; + psoDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS; + psoDesc.BlendState.RenderTarget[0].BlendEnable = FALSE; + psoDesc.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_ONE; + psoDesc.BlendState.RenderTarget[0].DestBlend = D3D12_BLEND_ZERO; + psoDesc.BlendState.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD; + psoDesc.BlendState.RenderTarget[0].SrcBlendAlpha = D3D12_BLEND_ONE; + psoDesc.BlendState.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_ZERO; + psoDesc.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD; + psoDesc.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; + + if (!gPipelineState.Initialize(device, psoDesc)) { + Log("[ERROR] Failed to initialize pipeline state"); + return false; + } + + struct Vertex { + float pos[4]; + float col[4]; + }; + + Vertex vertices[] = { + { { 0.0f, 0.5f, 0.0f, 1.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } }, + { { -0.5f, -0.5f, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } }, + { { 0.5f, -0.5f, 0.0f, 1.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }, + }; + + if (!gVertexBuffer.InitializeWithData(device, gCommandList.GetCommandList(), vertices, sizeof(vertices), D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER)) { + Log("[ERROR] Failed to initialize vertex buffer"); + return false; + } + gVertexBuffer.SetStride(sizeof(Vertex)); + gVertexBuffer.SetBufferType(BufferType::Vertex); + + Log("[INFO] D3D12 initialized successfully"); + return true; +} + +void WaitForGPU() { + gCommandQueue.WaitForIdle(); +} + +void ExecuteCommandList() { + gCommandList.Close(); + void* commandLists[] = { gCommandList.GetCommandList() }; + gCommandQueue.ExecuteCommandLists(1, commandLists); +} + +void BeginRender() { + gCurrentRTIndex = gSwapChain.GetCurrentBackBufferIndex(); + + D3D12Texture& currentBackBuffer = gSwapChain.GetBackBuffer(gCurrentRTIndex); + gCommandList.TransitionBarrier(currentBackBuffer.GetResource(), + ResourceStates::Present, ResourceStates::RenderTarget); + + CPUDescriptorHandle rtvCpuHandle = gRTVHeap.GetCPUDescriptorHandle(gCurrentRTIndex); + CPUDescriptorHandle dsvCpuHandle = gDSVHeap.GetCPUDescriptorHandle(0); + D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = { rtvCpuHandle.ptr }; + D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = { dsvCpuHandle.ptr }; + + gCommandList.SetRenderTargetsHandle(1, &rtvHandle, &dsvHandle); + + Viewport viewport = { 0.0f, 0.0f, (float)gWidth, (float)gHeight, 0.0f, 1.0f }; + Rect scissorRect = { 0, 0, gWidth, gHeight }; + gCommandList.SetViewport(viewport); + gCommandList.SetScissorRect(scissorRect); + + float clearColor[] = { 0.0f, 0.0f, 1.0f, 1.0f }; + gCommandList.ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr); + gCommandList.ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr); +} + +void EndRender() { + D3D12Texture& currentBackBuffer = gSwapChain.GetBackBuffer(gCurrentRTIndex); + gCommandList.TransitionBarrier(currentBackBuffer.GetResource(), + ResourceStates::RenderTarget, ResourceStates::Present); +} + +int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { + Logger::Get().Initialize(); + Logger::Get().AddSink(std::make_unique()); + Logger::Get().SetMinimumLevel(LogLevel::Debug); + + Log("[INFO] D3D12 Triangle Test Starting"); + + WNDCLASSEX wc = {}; + wc.cbSize = sizeof(WNDCLASSEX); + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = WindowProc; + wc.hInstance = hInstance; + wc.lpszClassName = L"D3D12TriangleTest"; + + if (!RegisterClassEx(&wc)) { + MessageBox(NULL, L"Failed to register window class", L"Error", MB_OK); + return -1; + } + + RECT rect = { 0, 0, gWidth, gHeight }; + AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE); + + gHWND = CreateWindowEx(0, L"D3D12TriangleTest", L"D3D12 Triangle Test", + WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, + rect.right - rect.left, rect.bottom - rect.top, + NULL, NULL, hInstance, NULL); + + if (!gHWND) { + MessageBox(NULL, L"Failed to create window", L"Error", MB_OK); + return -1; + } + + if (!InitD3D12()) { + MessageBox(NULL, L"Failed to initialize D3D12", L"Error", MB_OK); + return -1; + } + + ShowWindow(gHWND, nShowCmd); + UpdateWindow(gHWND); + + MSG msg = {}; + int frameCount = 0; + const int targetFrameCount = 30; + + while (true) { + if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } else { + if (frameCount > 0) { + gCommandQueue.WaitForPreviousFrame(); + } + + gCommandAllocator.Reset(); + gCommandList.Reset(); + + BeginRender(); + + gCommandList.SetPipelineState(gPipelineState.GetPipelineState()); + gCommandList.SetRootSignature(gRootSignature.GetRootSignature()); + gCommandList.SetPrimitiveTopology(PrimitiveTopology::TriangleList); + gCommandList.SetVertexBuffer(0, gVertexBuffer.GetResource(), 0, gVertexBuffer.GetStride()); + gCommandList.Draw(3, 1, 0, 0); + + frameCount++; + + if (frameCount >= targetFrameCount) { + Log("[INFO] Reached target frame count %d - taking screenshot before present!", targetFrameCount); + ExecuteCommandList(); + WaitForGPU(); + Log("[INFO] GPU idle, taking screenshot..."); + bool screenshotResult = D3D12Screenshot::Capture( + gDevice, + gCommandQueue, + gSwapChain.GetBackBuffer(gCurrentRTIndex), + "triangle.ppm" + ); + if (screenshotResult) { + Log("[INFO] Screenshot saved to triangle.ppm"); + } else { + Log("[ERROR] Screenshot failed"); + } + break; + } + + EndRender(); + ExecuteCommandList(); + gSwapChain.Present(0, 0); + } + } + + gCommandList.Shutdown(); + gCommandAllocator.Shutdown(); + gSwapChain.Shutdown(); + gDevice.Shutdown(); + + Logger::Get().Shutdown(); + + Log("[INFO] D3D12 Triangle Test Finished"); + return 0; +} \ No newline at end of file