Fix RHI texture binding and add pure quad test

This commit is contained in:
2026-03-26 00:47:12 +08:00
parent 76c4c2ace2
commit 9adac63b4c
20 changed files with 711 additions and 53 deletions

View File

@@ -142,6 +142,7 @@ inline DXGI_FORMAT ToD3D12(Format format) {
case Format::R32G32B32A32_Float: return DXGI_FORMAT_R32G32B32A32_FLOAT;
case Format::R16_Float: return DXGI_FORMAT_R16_FLOAT;
case Format::R32_Float: return DXGI_FORMAT_R32_FLOAT;
case Format::R32G32_Float: return DXGI_FORMAT_R32G32_FLOAT;
case Format::D16_UNorm: return DXGI_FORMAT_D16_UNORM;
case Format::D24_UNorm_S8_UInt: return DXGI_FORMAT_D24_UNORM_S8_UINT;
case Format::D32_Float: return DXGI_FORMAT_D32_FLOAT;
@@ -168,6 +169,7 @@ inline Format FromD3D12(DXGI_FORMAT format) {
case DXGI_FORMAT_R32G32B32A32_FLOAT: return Format::R32G32B32A32_Float;
case DXGI_FORMAT_R16_FLOAT: return Format::R16_Float;
case DXGI_FORMAT_R32_FLOAT: return Format::R32_Float;
case DXGI_FORMAT_R32G32_FLOAT: return Format::R32G32_Float;
case DXGI_FORMAT_D16_UNORM: return Format::D16_UNorm;
case DXGI_FORMAT_D24_UNORM_S8_UINT: return Format::D24_UNorm_S8_UInt;
case DXGI_FORMAT_D32_FLOAT: return Format::D32_Float;

View File

@@ -3,6 +3,7 @@
#include <d3d12.h>
#include <wrl/client.h>
#include <unordered_map>
#include <vector>
#include "../RHIPipelineLayout.h"
#include "D3D12RootSignature.h"
@@ -36,6 +37,7 @@ private:
D3D12Device* m_device;
std::unordered_map<uint32_t, uint32_t> m_registerToRootIndex;
std::vector<D3D12_ROOT_PARAMETER> m_rootParameters;
std::vector<D3D12_DESCRIPTOR_RANGE> m_descriptorRanges;
};
} // namespace RHI

View File

@@ -19,7 +19,7 @@ public:
bool Initialize(ID3D12Device* device, const D3D12_SAMPLER_DESC& desc);
void Shutdown() override;
D3D12_SAMPLER_DESC GetDesc() const { return m_desc; }
const D3D12_SAMPLER_DESC& GetDesc() const { return m_desc; }
void* GetNativeHandle() override { return &m_desc; }
unsigned int GetID() override { return m_id; }

View File

@@ -20,7 +20,8 @@ public:
bool Initialize(ID3D12Device* device, const D3D12_RESOURCE_DESC& desc, D3D12_RESOURCE_STATES initialState = D3D12_RESOURCE_STATE_COMMON);
bool InitializeFromExisting(ID3D12Resource* resource, bool ownsResource = false);
bool InitializeFromData(ID3D12Device* device, ID3D12GraphicsCommandList* commandList,
const void* pixelData, uint32_t width, uint32_t height, DXGI_FORMAT format, uint32_t rowPitch = 0);
const void* pixelData, uint32_t width, uint32_t height, DXGI_FORMAT format, uint32_t rowPitch = 0,
ComPtr<ID3D12Resource>* uploadBuffer = nullptr);
bool InitializeDepthStencil(ID3D12Device* device, uint32_t width, uint32_t height, DXGI_FORMAT format = DXGI_FORMAT_D24_UNORM_S8_UINT);
void Shutdown() override;

View File

@@ -126,6 +126,9 @@ inline void ToOpenGLFormat(OpenGLFormat fmt, GLint& internalFormat, GLenum& glFo
case OpenGLFormat::RG8:
internalFormat = GL_RG8; glFormat = GL_RG; glType = GL_UNSIGNED_BYTE;
break;
case OpenGLFormat::RG32F:
internalFormat = GL_RG32F; glFormat = GL_RG; glType = GL_FLOAT;
break;
case OpenGLFormat::RGBA8:
internalFormat = GL_RGBA8; glFormat = GL_RGBA; glType = GL_UNSIGNED_BYTE;
break;

View File

@@ -20,6 +20,7 @@ enum class OpenGLTextureType {
enum class OpenGLFormat {
R8,
RG8,
RG32F,
RGBA8,
RGBA16F,
RGBA32F,
@@ -32,6 +33,7 @@ enum class OpenGLFormat {
enum class OpenGLInternalFormat {
R8 = 1,
RG8 = 2,
RG32F = 13,
RGBA8 = 4,
RGBA16F = 11,
RGBA32F = 16,

View File

@@ -314,7 +314,8 @@ enum class Format : uint32_t {
BC6H_UF16,
BC7_UNorm,
R32G32B32A32_UInt,
R32_UInt
R32_UInt,
R32G32_Float
};
enum class ResourceStates : uint32_t {

View File

@@ -37,6 +37,7 @@ bool D3D12DescriptorHeap::Initialize(ID3D12Device* device, DescriptorHeapType ty
return false;
}
m_device = device;
m_type = type;
m_numDescriptors = numDescriptors;
m_shaderVisible = shaderVisible;
@@ -47,6 +48,7 @@ bool D3D12DescriptorHeap::Initialize(ID3D12Device* device, DescriptorHeapType ty
}
void D3D12DescriptorHeap::Shutdown() {
m_device.Reset();
m_descriptorHeap.Reset();
m_allocatedSets.clear();
m_numDescriptors = 0;

View File

@@ -74,6 +74,8 @@ uint32_t GetFormatBytesPerPixel(Format format) {
return 8;
case Format::R32_Float:
return 4;
case Format::R32G32_Float:
return 8;
case Format::R32G32B32A32_Float:
return 16;
default:
@@ -390,6 +392,7 @@ RHITexture* D3D12Device::CreateTexture(const TextureDesc& desc, const void* init
uploadCommandList.Reset();
auto* texture = new D3D12Texture();
ComPtr<ID3D12Resource> uploadBuffer;
if (!texture->InitializeFromData(
m_device.Get(),
uploadCommandList.GetCommandList(),
@@ -397,7 +400,8 @@ RHITexture* D3D12Device::CreateTexture(const TextureDesc& desc, const void* init
desc.width,
desc.height,
ToD3D12(format),
resolvedRowPitch)) {
resolvedRowPitch,
&uploadBuffer)) {
delete texture;
uploadCommandList.Shutdown();
uploadAllocator.Shutdown();
@@ -411,6 +415,7 @@ RHITexture* D3D12Device::CreateTexture(const TextureDesc& desc, const void* init
ID3D12CommandList* commandLists[] = { uploadCommandList.GetCommandList() };
uploadQueue.ExecuteCommandListsInternal(1, commandLists);
uploadQueue.WaitForIdle();
uploadBuffer.Reset();
uploadCommandList.Shutdown();
uploadAllocator.Shutdown();
@@ -442,13 +447,13 @@ RHIShader* D3D12Device::CreateShader(const ShaderCompileDesc& desc) {
RHISampler* D3D12Device::CreateSampler(const SamplerDesc& desc) {
auto* sampler = new D3D12Sampler();
D3D12_SAMPLER_DESC d3d12Desc = {};
d3d12Desc.Filter = static_cast<D3D12_FILTER>(desc.filter);
d3d12Desc.AddressU = static_cast<D3D12_TEXTURE_ADDRESS_MODE>(desc.addressU);
d3d12Desc.AddressV = static_cast<D3D12_TEXTURE_ADDRESS_MODE>(desc.addressV);
d3d12Desc.AddressW = static_cast<D3D12_TEXTURE_ADDRESS_MODE>(desc.addressW);
d3d12Desc.Filter = ToD3D12(static_cast<FilterMode>(desc.filter));
d3d12Desc.AddressU = ToD3D12(static_cast<TextureAddressMode>(desc.addressU));
d3d12Desc.AddressV = ToD3D12(static_cast<TextureAddressMode>(desc.addressV));
d3d12Desc.AddressW = ToD3D12(static_cast<TextureAddressMode>(desc.addressW));
d3d12Desc.MipLODBias = desc.mipLodBias;
d3d12Desc.MaxAnisotropy = desc.maxAnisotropy;
d3d12Desc.ComparisonFunc = static_cast<D3D12_COMPARISON_FUNC>(desc.comparisonFunc);
d3d12Desc.ComparisonFunc = ToD3D12(static_cast<ComparisonFunc>(desc.comparisonFunc));
d3d12Desc.BorderColor[0] = desc.borderColorR;
d3d12Desc.BorderColor[1] = desc.borderColorG;
d3d12Desc.BorderColor[2] = desc.borderColorB;
@@ -735,7 +740,7 @@ RHIResourceView* D3D12Device::CreateShaderResourceView(RHITexture* texture, cons
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
auto heap = std::make_unique<D3D12DescriptorHeap>();
if (!heap->Initialize(m_device.Get(), DescriptorHeapType::CBV_SRV_UAV, 1, true)) {
if (!heap->Initialize(m_device.Get(), DescriptorHeapType::CBV_SRV_UAV, 1, false)) {
delete view;
return nullptr;
}
@@ -756,7 +761,7 @@ RHIResourceView* D3D12Device::CreateUnorderedAccessView(RHITexture* texture, con
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
auto heap = std::make_unique<D3D12DescriptorHeap>();
if (!heap->Initialize(m_device.Get(), DescriptorHeapType::CBV_SRV_UAV, 1, true)) {
if (!heap->Initialize(m_device.Get(), DescriptorHeapType::CBV_SRV_UAV, 1, false)) {
delete view;
return nullptr;
}

View File

@@ -24,8 +24,20 @@ bool D3D12PipelineLayout::InitializeInternal(D3D12Device* device, const RHIPipel
m_device = device;
m_rootParameters.clear();
m_descriptorRanges.clear();
m_registerToRootIndex.clear();
const uint32_t rootParameterCount =
(desc.constantBufferCount > 0 ? 1u : 0u) +
(desc.textureCount > 0 ? 1u : 0u) +
(desc.samplerCount > 0 ? 1u : 0u);
const uint32_t descriptorRangeCount =
(desc.textureCount > 0 ? 1u : 0u) +
(desc.samplerCount > 0 ? 1u : 0u);
m_rootParameters.reserve(rootParameterCount);
m_descriptorRanges.reserve(descriptorRangeCount);
uint32_t rootIndex = 0;
if (desc.constantBufferCount > 0) {
@@ -39,9 +51,10 @@ bool D3D12PipelineLayout::InitializeInternal(D3D12Device* device, const RHIPipel
}
if (desc.textureCount > 0) {
D3D12_DESCRIPTOR_RANGE range = D3D12RootSignature::CreateDescriptorRange(
D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 0, desc.textureCount, 0);
D3D12_ROOT_PARAMETER param = D3D12RootSignature::CreateDescriptorTable(1, &range, ShaderVisibility::All);
m_descriptorRanges.push_back(D3D12RootSignature::CreateDescriptorRange(
D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 0, desc.textureCount, 0));
D3D12_ROOT_PARAMETER param = D3D12RootSignature::CreateDescriptorTable(
1, &m_descriptorRanges.back(), ShaderVisibility::All);
m_rootParameters.push_back(param);
for (uint32_t i = 0; i < desc.textureCount; ++i) {
m_registerToRootIndex[100 + i] = rootIndex;
@@ -50,9 +63,10 @@ bool D3D12PipelineLayout::InitializeInternal(D3D12Device* device, const RHIPipel
}
if (desc.samplerCount > 0) {
D3D12_DESCRIPTOR_RANGE range = D3D12RootSignature::CreateDescriptorRange(
D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 0, desc.samplerCount, 0);
D3D12_ROOT_PARAMETER param = D3D12RootSignature::CreateDescriptorTable(1, &range, ShaderVisibility::All);
m_descriptorRanges.push_back(D3D12RootSignature::CreateDescriptorRange(
D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 0, desc.samplerCount, 0));
D3D12_ROOT_PARAMETER param = D3D12RootSignature::CreateDescriptorTable(
1, &m_descriptorRanges.back(), ShaderVisibility::All);
m_rootParameters.push_back(param);
for (uint32_t i = 0; i < desc.samplerCount; ++i) {
m_registerToRootIndex[200 + i] = rootIndex;
@@ -99,6 +113,7 @@ bool D3D12PipelineLayout::InitializeInternal(D3D12Device* device, const RHIPipel
void D3D12PipelineLayout::Shutdown() {
m_rootSignature.Reset();
m_rootParameters.clear();
m_descriptorRanges.clear();
m_registerToRootIndex.clear();
m_device = nullptr;
}

View File

@@ -44,7 +44,8 @@ bool D3D12Texture::InitializeFromExisting(ID3D12Resource* resource, bool ownsRes
}
bool D3D12Texture::InitializeFromData(ID3D12Device* device, ID3D12GraphicsCommandList* commandList,
const void* pixelData, uint32_t width, uint32_t height, DXGI_FORMAT format, uint32_t rowPitch) {
const void* pixelData, uint32_t width, uint32_t height, DXGI_FORMAT format, uint32_t rowPitch,
ComPtr<ID3D12Resource>* uploadBuffer) {
D3D12_RESOURCE_DESC textureDesc = {};
textureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
@@ -84,7 +85,7 @@ bool D3D12Texture::InitializeFromData(ID3D12Device* device, ID3D12GraphicsComman
device->GetCopyableFootprints(&textureDesc, 0, 1, 0,
&subresourceFootprint, &rowUsed, &rowSizeInBytes, &memorySizeUsed);
ID3D12Resource* tempBufferObject = nullptr;
ComPtr<ID3D12Resource> tempBufferObject;
D3D12_HEAP_PROPERTIES d3dTempHeapProperties = {};
d3dTempHeapProperties.Type = D3D12_HEAP_TYPE_UPLOAD;
@@ -107,7 +108,7 @@ bool D3D12Texture::InitializeFromData(ID3D12Device* device, ID3D12GraphicsComman
&d3d12TempResourceDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&tempBufferObject)
IID_PPV_ARGS(tempBufferObject.GetAddressOf())
);
if (FAILED(hResult)) {
@@ -130,7 +131,7 @@ bool D3D12Texture::InitializeFromData(ID3D12Device* device, ID3D12GraphicsComman
dst.SubresourceIndex = 0;
D3D12_TEXTURE_COPY_LOCATION src = {};
src.pResource = tempBufferObject;
src.pResource = tempBufferObject.Get();
src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
src.PlacedFootprint = subresourceFootprint;
commandList->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
@@ -145,7 +146,9 @@ bool D3D12Texture::InitializeFromData(ID3D12Device* device, ID3D12GraphicsComman
commandList->ResourceBarrier(1, &barrier);
m_state = ResourceStates::PixelShaderResource;
tempBufferObject->Release();
if (uploadBuffer != nullptr) {
*uploadBuffer = tempBufferObject;
}
return true;
}

View File

@@ -40,6 +40,9 @@ bool GetOpenGLVertexAttribFormat(Format format, OpenGLVertexAttribFormat& attrib
case Format::R32_Float:
attributeFormat = { 1, GL_FLOAT, GL_FALSE, false };
return true;
case Format::R32G32_Float:
attributeFormat = { 2, GL_FLOAT, GL_FALSE, false };
return true;
case Format::R32G32B32A32_Float:
attributeFormat = { 4, GL_FLOAT, GL_FALSE, false };
return true;

View File

@@ -61,6 +61,29 @@ std::string SourceToString(const ShaderCompileDesc& desc) {
return std::string(reinterpret_cast<const char*>(desc.source.data()), desc.source.size());
}
OpenGLFormat ToOpenGLTextureFormat(Format format) {
switch (format) {
case Format::R8_UNorm:
return OpenGLFormat::R8;
case Format::R8G8_UNorm:
return OpenGLFormat::RG8;
case Format::R32G32_Float:
return OpenGLFormat::RG32F;
case Format::R8G8B8A8_UNorm:
return OpenGLFormat::RGBA8;
case Format::R16G16B16A16_Float:
return OpenGLFormat::RGBA16F;
case Format::R32G32B32A32_Float:
return OpenGLFormat::RGBA32F;
case Format::D24_UNorm_S8_UInt:
return OpenGLFormat::Depth24Stencil8;
case Format::D32_Float:
return OpenGLFormat::Depth32F;
default:
return OpenGLFormat::RGBA8;
}
}
} // namespace
OpenGLDevice::OpenGLDevice()
@@ -338,19 +361,7 @@ RHITexture* OpenGLDevice::CreateTexture(const TextureDesc& desc) {
break;
}
OpenGLFormat format = OpenGLFormat::RGBA8;
switch (desc.format) {
case 1: format = OpenGLFormat::R8; break;
case 2: format = OpenGLFormat::RG8; break;
case 3: format = OpenGLFormat::RGBA8; break;
case 4: format = OpenGLFormat::RGBA16F; break;
case 5: format = OpenGLFormat::RGBA32F; break;
case 6: format = OpenGLFormat::RGBA16F; break;
case 7: format = OpenGLFormat::RGBA32F; break;
case 8: format = OpenGLFormat::Depth24Stencil8; break;
case 9: format = OpenGLFormat::Depth32F; break;
default: format = OpenGLFormat::RGBA8; break;
}
OpenGLFormat format = ToOpenGLTextureFormat(static_cast<Format>(desc.format));
texture->Initialize(type, desc.width, desc.height, desc.depth, desc.mipLevels, format, nullptr);
texture->SetFormat(static_cast<Format>(desc.format));
return texture;
@@ -385,19 +396,7 @@ RHITexture* OpenGLDevice::CreateTexture(const TextureDesc& desc, const void* ini
break;
}
OpenGLFormat format = OpenGLFormat::RGBA8;
switch (desc.format) {
case 1: format = OpenGLFormat::R8; break;
case 2: format = OpenGLFormat::RG8; break;
case 3: format = OpenGLFormat::RGBA8; break;
case 4: format = OpenGLFormat::RGBA16F; break;
case 5: format = OpenGLFormat::RGBA32F; break;
case 6: format = OpenGLFormat::RGBA16F; break;
case 7: format = OpenGLFormat::RGBA32F; break;
case 8: format = OpenGLFormat::Depth24Stencil8; break;
case 9: format = OpenGLFormat::Depth32F; break;
default: format = OpenGLFormat::RGBA8; break;
}
OpenGLFormat format = ToOpenGLTextureFormat(static_cast<Format>(desc.format));
if (!texture->Initialize(type, desc.width, desc.height, desc.depth, desc.mipLevels, format, initialData)) {
delete texture;

View File

@@ -6,3 +6,4 @@ enable_testing()
add_subdirectory(minimal)
add_subdirectory(triangle)
add_subdirectory(quad)

View File

@@ -0,0 +1,59 @@
cmake_minimum_required(VERSION 3.15)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
project(rhi_integration_quad)
set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine)
set(PACKAGE_DIR ${CMAKE_SOURCE_DIR}/tests/opengl/package)
get_filename_component(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE)
add_executable(rhi_integration_quad
main.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../fixtures/RHIIntegrationFixture.cpp
${PACKAGE_DIR}/src/glad.c
)
target_include_directories(rhi_integration_quad PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/../fixtures
${ENGINE_ROOT_DIR}/include
${PACKAGE_DIR}/include
${PROJECT_ROOT_DIR}/engine/src
)
target_link_libraries(rhi_integration_quad PRIVATE
d3d12
dxgi
d3dcompiler
winmm
opengl32
XCEngine
GTest::gtest
)
target_compile_definitions(rhi_integration_quad PRIVATE
UNICODE
_UNICODE
XCENGINE_SUPPORT_OPENGL
XCENGINE_SUPPORT_D3D12
)
add_custom_command(TARGET rhi_integration_quad POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/Res
$<TARGET_FILE_DIR:rhi_integration_quad>/Res
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/integration/compare_ppm.py
$<TARGET_FILE_DIR:rhi_integration_quad>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
$<TARGET_FILE_DIR:rhi_integration_quad>/GT.ppm
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll
$<TARGET_FILE_DIR:rhi_integration_quad>/
)
include(GoogleTest)
gtest_discover_tests(rhi_integration_quad)

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -0,0 +1,521 @@
#include <windows.h>
#include <filesystem>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <gtest/gtest.h>
#include <stb/stb_image.h>
#include "../fixtures/RHIIntegrationFixture.h"
#include "XCEngine/Debug/ConsoleLogSink.h"
#include "XCEngine/Debug/Logger.h"
#include "XCEngine/RHI/RHIBuffer.h"
#include "XCEngine/RHI/RHIDescriptorPool.h"
#include "XCEngine/RHI/RHIDescriptorSet.h"
#include "XCEngine/RHI/RHIPipelineLayout.h"
#include "XCEngine/RHI/RHIPipelineState.h"
#include "XCEngine/RHI/RHIResourceView.h"
#include "XCEngine/RHI/RHISampler.h"
#include "XCEngine/RHI/RHITexture.h"
using namespace XCEngine::Debug;
using namespace XCEngine::RHI;
using namespace XCEngine::RHI::Integration;
namespace {
struct Vertex {
float pos[4];
float uv[2];
};
constexpr Vertex kQuadVertices[] = {
{ { -0.5f, -0.5f, 0.0f, 1.0f }, { 0.0f, 1.0f } },
{ { -0.5f, 0.5f, 0.0f, 1.0f }, { 0.0f, 0.0f } },
{ { 0.5f, -0.5f, 0.0f, 1.0f }, { 1.0f, 1.0f } },
{ { 0.5f, 0.5f, 0.0f, 1.0f }, { 1.0f, 0.0f } },
};
constexpr uint32_t kQuadIndices[] = { 0, 1, 2, 2, 1, 3 };
std::filesystem::path GetExecutableDirectory() {
char exePath[MAX_PATH] = {};
const DWORD length = GetModuleFileNameA(nullptr, exePath, MAX_PATH);
if (length == 0 || length >= MAX_PATH) {
return std::filesystem::current_path();
}
return std::filesystem::path(exePath).parent_path();
}
std::filesystem::path ResolveRuntimePath(const char* relativePath) {
return GetExecutableDirectory() / relativePath;
}
const char kQuadHlsl[] = R"(
Texture2D gTexture : register(t0);
SamplerState gSampler : register(s0);
struct VSInput {
float4 position : POSITION;
float2 texcoord : TEXCOORD;
};
struct PSInput {
float4 position : SV_POSITION;
float2 texcoord : TEXCOORD;
};
PSInput MainVS(VSInput input) {
PSInput output;
output.position = input.position;
output.texcoord = input.texcoord;
return output;
}
float4 MainPS(PSInput input) : SV_TARGET {
return gTexture.Sample(gSampler, input.texcoord);
}
)";
const char kQuadVertexShader[] = R"(#version 430
layout(location = 0) in vec4 aPosition;
layout(location = 1) in vec2 aTexCoord;
out vec2 vTexCoord;
void main() {
gl_Position = aPosition;
vTexCoord = aTexCoord;
}
)";
const char kQuadFragmentShader[] = R"(#version 430
layout(binding = 0) uniform sampler2D uTexture;
in vec2 vTexCoord;
layout(location = 0) out vec4 fragColor;
void main() {
fragColor = texture(uTexture, vTexCoord);
}
)";
const char* GetScreenshotFilename(RHIType type) {
return type == RHIType::D3D12 ? "quad_d3d12.ppm" : "quad_opengl.ppm";
}
RHITexture* LoadQuadTexture(RHIDevice* device) {
const std::filesystem::path texturePath = ResolveRuntimePath("Res/Image/earth.png");
const std::string texturePathString = texturePath.string();
stbi_set_flip_vertically_on_load(0);
int width = 0;
int height = 0;
int channels = 0;
stbi_uc* pixels = stbi_load(texturePathString.c_str(), &width, &height, &channels, STBI_rgb_alpha);
if (pixels == nullptr) {
Log("[TEST] Failed to load quad texture: %s", texturePathString.c_str());
return nullptr;
}
TextureDesc textureDesc = {};
textureDesc.width = static_cast<uint32_t>(width);
textureDesc.height = static_cast<uint32_t>(height);
textureDesc.depth = 1;
textureDesc.mipLevels = 1;
textureDesc.arraySize = 1;
textureDesc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
textureDesc.textureType = static_cast<uint32_t>(TextureType::Texture2D);
textureDesc.sampleCount = 1;
textureDesc.sampleQuality = 0;
textureDesc.flags = 0;
RHITexture* texture = device->CreateTexture(
textureDesc,
pixels,
static_cast<size_t>(width) * static_cast<size_t>(height) * 4,
static_cast<uint32_t>(width) * 4);
stbi_image_free(pixels);
return texture;
}
int GetComparisonThreshold(RHIType type) {
return type == RHIType::OpenGL ? 5 : 0;
}
GraphicsPipelineDesc CreateQuadPipelineDesc(RHIType type, RHIPipelineLayout* pipelineLayout) {
GraphicsPipelineDesc desc = {};
desc.pipelineLayout = pipelineLayout;
desc.topologyType = static_cast<uint32_t>(PrimitiveTopologyType::Triangle);
desc.renderTargetFormats[0] = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
desc.depthStencilFormat = static_cast<uint32_t>(Format::Unknown);
desc.sampleCount = 1;
desc.rasterizerState.fillMode = static_cast<uint32_t>(FillMode::Solid);
desc.rasterizerState.cullMode = static_cast<uint32_t>(CullMode::None);
desc.rasterizerState.frontFace = static_cast<uint32_t>(FrontFace::CounterClockwise);
desc.rasterizerState.depthClipEnable = true;
desc.depthStencilState.depthTestEnable = false;
desc.depthStencilState.depthWriteEnable = false;
desc.depthStencilState.stencilEnable = false;
InputElementDesc position = {};
position.semanticName = "POSITION";
position.semanticIndex = 0;
position.format = static_cast<uint32_t>(Format::R32G32B32A32_Float);
position.inputSlot = 0;
position.alignedByteOffset = 0;
desc.inputLayout.elements.push_back(position);
InputElementDesc texcoord = {};
texcoord.semanticName = "TEXCOORD";
texcoord.semanticIndex = 0;
texcoord.format = static_cast<uint32_t>(Format::R32G32_Float);
texcoord.inputSlot = 0;
texcoord.alignedByteOffset = sizeof(float) * 4;
desc.inputLayout.elements.push_back(texcoord);
if (type == RHIType::D3D12) {
desc.vertexShader.source.assign(kQuadHlsl, kQuadHlsl + strlen(kQuadHlsl));
desc.vertexShader.sourceLanguage = ShaderLanguage::HLSL;
desc.vertexShader.entryPoint = L"MainVS";
desc.vertexShader.profile = L"vs_5_0";
desc.fragmentShader.source.assign(kQuadHlsl, kQuadHlsl + strlen(kQuadHlsl));
desc.fragmentShader.sourceLanguage = ShaderLanguage::HLSL;
desc.fragmentShader.entryPoint = L"MainPS";
desc.fragmentShader.profile = L"ps_5_0";
} else {
desc.vertexShader.source.assign(kQuadVertexShader, kQuadVertexShader + strlen(kQuadVertexShader));
desc.vertexShader.sourceLanguage = ShaderLanguage::GLSL;
desc.vertexShader.profile = L"vs_4_30";
desc.fragmentShader.source.assign(kQuadFragmentShader, kQuadFragmentShader + strlen(kQuadFragmentShader));
desc.fragmentShader.sourceLanguage = ShaderLanguage::GLSL;
desc.fragmentShader.profile = L"fs_4_30";
}
return desc;
}
class QuadTest : public RHIIntegrationFixture {
protected:
void SetUp() override;
void TearDown() override;
void RenderFrame() override;
private:
void InitializeQuadResources();
void ShutdownQuadResources();
RHIBuffer* mVertexBuffer = nullptr;
RHIResourceView* mVertexBufferView = nullptr;
RHIBuffer* mIndexBuffer = nullptr;
RHIResourceView* mIndexBufferView = nullptr;
RHITexture* mTexture = nullptr;
RHIResourceView* mTextureView = nullptr;
RHISampler* mSampler = nullptr;
RHIDescriptorPool* mTexturePool = nullptr;
RHIDescriptorSet* mTextureSet = nullptr;
RHIDescriptorPool* mSamplerPool = nullptr;
RHIDescriptorSet* mSamplerSet = nullptr;
RHIPipelineLayout* mPipelineLayout = nullptr;
RHIPipelineState* mPipelineState = nullptr;
};
void QuadTest::SetUp() {
RHIIntegrationFixture::SetUp();
InitializeQuadResources();
}
void QuadTest::TearDown() {
ShutdownQuadResources();
RHIIntegrationFixture::TearDown();
}
void QuadTest::InitializeQuadResources() {
BufferDesc vertexBufferDesc = {};
vertexBufferDesc.size = sizeof(kQuadVertices);
vertexBufferDesc.stride = sizeof(Vertex);
vertexBufferDesc.bufferType = static_cast<uint32_t>(BufferType::Vertex);
vertexBufferDesc.flags = 0;
mVertexBuffer = GetDevice()->CreateBuffer(vertexBufferDesc);
ASSERT_NE(mVertexBuffer, nullptr);
mVertexBuffer->SetData(kQuadVertices, sizeof(kQuadVertices));
mVertexBuffer->SetStride(sizeof(Vertex));
mVertexBuffer->SetBufferType(BufferType::Vertex);
ResourceViewDesc vertexViewDesc = {};
vertexViewDesc.dimension = ResourceViewDimension::Buffer;
vertexViewDesc.structureByteStride = sizeof(Vertex);
mVertexBufferView = GetDevice()->CreateVertexBufferView(mVertexBuffer, vertexViewDesc);
ASSERT_NE(mVertexBufferView, nullptr);
BufferDesc indexBufferDesc = {};
indexBufferDesc.size = sizeof(kQuadIndices);
indexBufferDesc.stride = sizeof(uint32_t);
indexBufferDesc.bufferType = static_cast<uint32_t>(BufferType::Index);
indexBufferDesc.flags = 0;
mIndexBuffer = GetDevice()->CreateBuffer(indexBufferDesc);
ASSERT_NE(mIndexBuffer, nullptr);
mIndexBuffer->SetData(kQuadIndices, sizeof(kQuadIndices));
mIndexBuffer->SetStride(sizeof(uint32_t));
mIndexBuffer->SetBufferType(BufferType::Index);
ResourceViewDesc indexViewDesc = {};
indexViewDesc.dimension = ResourceViewDimension::Buffer;
indexViewDesc.format = static_cast<uint32_t>(Format::R32_UInt);
mIndexBufferView = GetDevice()->CreateIndexBufferView(mIndexBuffer, indexViewDesc);
ASSERT_NE(mIndexBufferView, nullptr);
mTexture = LoadQuadTexture(GetDevice());
ASSERT_NE(mTexture, nullptr);
ResourceViewDesc textureViewDesc = {};
textureViewDesc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
textureViewDesc.dimension = ResourceViewDimension::Texture2D;
textureViewDesc.mipLevel = 0;
mTextureView = GetDevice()->CreateShaderResourceView(mTexture, textureViewDesc);
ASSERT_NE(mTextureView, nullptr);
SamplerDesc samplerDesc = {};
samplerDesc.filter = static_cast<uint32_t>(FilterMode::Linear);
samplerDesc.addressU = static_cast<uint32_t>(TextureAddressMode::Clamp);
samplerDesc.addressV = static_cast<uint32_t>(TextureAddressMode::Clamp);
samplerDesc.addressW = static_cast<uint32_t>(TextureAddressMode::Clamp);
samplerDesc.mipLodBias = 0.0f;
samplerDesc.maxAnisotropy = 1;
samplerDesc.comparisonFunc = static_cast<uint32_t>(ComparisonFunc::Always);
samplerDesc.borderColorR = 0.0f;
samplerDesc.borderColorG = 0.0f;
samplerDesc.borderColorB = 0.0f;
samplerDesc.borderColorA = 0.0f;
samplerDesc.minLod = 0.0f;
samplerDesc.maxLod = 1000.0f;
mSampler = GetDevice()->CreateSampler(samplerDesc);
ASSERT_NE(mSampler, nullptr);
DescriptorPoolDesc texturePoolDesc = {};
texturePoolDesc.type = DescriptorHeapType::CBV_SRV_UAV;
texturePoolDesc.descriptorCount = 1;
texturePoolDesc.shaderVisible = true;
mTexturePool = GetDevice()->CreateDescriptorPool(texturePoolDesc);
ASSERT_NE(mTexturePool, nullptr);
DescriptorSetLayoutBinding textureBinding = {};
textureBinding.binding = 0;
textureBinding.type = static_cast<uint32_t>(DescriptorType::SRV);
textureBinding.count = 1;
DescriptorSetLayoutDesc textureLayoutDesc = {};
textureLayoutDesc.bindings = &textureBinding;
textureLayoutDesc.bindingCount = 1;
mTextureSet = mTexturePool->AllocateSet(textureLayoutDesc);
ASSERT_NE(mTextureSet, nullptr);
mTextureSet->Update(0, mTextureView);
DescriptorPoolDesc samplerPoolDesc = {};
samplerPoolDesc.type = DescriptorHeapType::Sampler;
samplerPoolDesc.descriptorCount = 1;
samplerPoolDesc.shaderVisible = true;
mSamplerPool = GetDevice()->CreateDescriptorPool(samplerPoolDesc);
ASSERT_NE(mSamplerPool, nullptr);
DescriptorSetLayoutBinding samplerBinding = {};
samplerBinding.binding = 0;
samplerBinding.type = static_cast<uint32_t>(DescriptorType::Sampler);
samplerBinding.count = 1;
DescriptorSetLayoutDesc samplerLayoutDesc = {};
samplerLayoutDesc.bindings = &samplerBinding;
samplerLayoutDesc.bindingCount = 1;
mSamplerSet = mSamplerPool->AllocateSet(samplerLayoutDesc);
ASSERT_NE(mSamplerSet, nullptr);
mSamplerSet->UpdateSampler(0, mSampler);
RHIPipelineLayoutDesc pipelineLayoutDesc = {};
pipelineLayoutDesc.textureCount = 1;
pipelineLayoutDesc.samplerCount = 1;
mPipelineLayout = GetDevice()->CreatePipelineLayout(pipelineLayoutDesc);
ASSERT_NE(mPipelineLayout, nullptr);
GraphicsPipelineDesc pipelineDesc = CreateQuadPipelineDesc(GetBackendType(), mPipelineLayout);
mPipelineState = GetDevice()->CreatePipelineState(pipelineDesc);
ASSERT_NE(mPipelineState, nullptr);
ASSERT_TRUE(mPipelineState->IsValid());
Log("[TEST] Quad resources initialized for backend %d", static_cast<int>(GetBackendType()));
}
void QuadTest::ShutdownQuadResources() {
if (mPipelineState != nullptr) {
mPipelineState->Shutdown();
delete mPipelineState;
mPipelineState = nullptr;
}
if (mPipelineLayout != nullptr) {
mPipelineLayout->Shutdown();
delete mPipelineLayout;
mPipelineLayout = nullptr;
}
if (mTextureSet != nullptr) {
mTextureSet->Shutdown();
delete mTextureSet;
mTextureSet = nullptr;
}
if (mSamplerSet != nullptr) {
mSamplerSet->Shutdown();
delete mSamplerSet;
mSamplerSet = nullptr;
}
if (mTexturePool != nullptr) {
mTexturePool->Shutdown();
delete mTexturePool;
mTexturePool = nullptr;
}
if (mSamplerPool != nullptr) {
mSamplerPool->Shutdown();
delete mSamplerPool;
mSamplerPool = nullptr;
}
if (mSampler != nullptr) {
mSampler->Shutdown();
delete mSampler;
mSampler = nullptr;
}
if (mTextureView != nullptr) {
mTextureView->Shutdown();
delete mTextureView;
mTextureView = nullptr;
}
if (mTexture != nullptr) {
mTexture->Shutdown();
delete mTexture;
mTexture = nullptr;
}
if (mVertexBufferView != nullptr) {
mVertexBufferView->Shutdown();
delete mVertexBufferView;
mVertexBufferView = nullptr;
}
if (mIndexBufferView != nullptr) {
mIndexBufferView->Shutdown();
delete mIndexBufferView;
mIndexBufferView = nullptr;
}
if (mVertexBuffer != nullptr) {
mVertexBuffer->Shutdown();
delete mVertexBuffer;
mVertexBuffer = nullptr;
}
if (mIndexBuffer != nullptr) {
mIndexBuffer->Shutdown();
delete mIndexBuffer;
mIndexBuffer = nullptr;
}
}
void QuadTest::RenderFrame() {
ASSERT_NE(mPipelineState, nullptr);
ASSERT_NE(mVertexBufferView, nullptr);
ASSERT_NE(mIndexBufferView, nullptr);
ASSERT_NE(mTextureSet, nullptr);
ASSERT_NE(mSamplerSet, nullptr);
ASSERT_NE(mPipelineLayout, nullptr);
RHICommandList* cmdList = GetCommandList();
RHICommandQueue* cmdQueue = GetCommandQueue();
ASSERT_NE(cmdList, nullptr);
ASSERT_NE(cmdQueue, nullptr);
cmdList->Reset();
SetRenderTargetForClear();
Viewport viewport = { 0.0f, 0.0f, 1280.0f, 720.0f, 0.0f, 1.0f };
Rect scissorRect = { 0, 0, 1280, 720 };
cmdList->SetViewport(viewport);
cmdList->SetScissorRect(scissorRect);
cmdList->Clear(0.0f, 0.0f, 1.0f, 1.0f, 1);
cmdList->SetPipelineState(mPipelineState);
RHIDescriptorSet* descriptorSets[] = { mTextureSet, mSamplerSet };
cmdList->SetGraphicsDescriptorSets(0, 2, descriptorSets, mPipelineLayout);
cmdList->SetPrimitiveTopology(PrimitiveTopology::TriangleList);
RHIResourceView* vertexBuffers[] = { mVertexBufferView };
uint64_t offsets[] = { 0 };
uint32_t strides[] = { sizeof(Vertex) };
cmdList->SetVertexBuffers(0, 1, vertexBuffers, offsets, strides);
cmdList->SetIndexBuffer(mIndexBufferView, 0);
cmdList->DrawIndexed(static_cast<uint32_t>(sizeof(kQuadIndices) / sizeof(kQuadIndices[0])));
EndRender();
cmdList->Close();
void* cmdLists[] = { cmdList };
cmdQueue->ExecuteCommandLists(1, cmdLists);
}
TEST_P(QuadTest, RenderQuad) {
RHICommandQueue* cmdQueue = GetCommandQueue();
RHISwapChain* swapChain = GetSwapChain();
const int targetFrameCount = 30;
const char* screenshotFilename = GetScreenshotFilename(GetBackendType());
const int comparisonThreshold = GetComparisonThreshold(GetBackendType());
for (int frameCount = 0; frameCount <= targetFrameCount; ++frameCount) {
if (frameCount > 0) {
cmdQueue->WaitForPreviousFrame();
}
Log("[TEST] Quad MainLoop: frame %d", frameCount);
BeginRender();
RenderFrame();
if (frameCount >= targetFrameCount) {
cmdQueue->WaitForIdle();
Log("[TEST] Quad MainLoop: frame %d reached, capturing %s", frameCount, screenshotFilename);
ASSERT_TRUE(TakeScreenshot(screenshotFilename));
ASSERT_TRUE(CompareWithGoldenTemplate(screenshotFilename, "GT.ppm", static_cast<float>(comparisonThreshold)));
Log("[TEST] Quad MainLoop: frame %d compare passed", frameCount);
break;
}
swapChain->Present(0, 0);
}
}
} // namespace
INSTANTIATE_TEST_SUITE_P(D3D12, QuadTest, ::testing::Values(RHIType::D3D12));
INSTANTIATE_TEST_SUITE_P(OpenGL, QuadTest, ::testing::Values(RHIType::OpenGL));
GTEST_API_ int main(int argc, char** argv) {
Logger::Get().Initialize();
Logger::Get().AddSink(std::make_unique<ConsoleLogSink>());
Logger::Get().SetMinimumLevel(LogLevel::Debug);
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@@ -122,6 +122,38 @@ TEST_P(RHITestFixture, Device_CreateUnorderedAccessView) {
delete texture;
}
TEST_P(RHITestFixture, Device_CreateShaderResourceView_FromInitialDataTexture) {
TextureDesc texDesc = {};
texDesc.width = 1;
texDesc.height = 1;
texDesc.depth = 1;
texDesc.mipLevels = 1;
texDesc.arraySize = 1;
texDesc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
texDesc.textureType = static_cast<uint32_t>(TextureType::Texture2D);
texDesc.sampleCount = 1;
texDesc.sampleQuality = 0;
texDesc.flags = 0;
const uint8_t pixel[4] = { 255, 255, 255, 255 };
RHITexture* texture = GetDevice()->CreateTexture(texDesc, pixel, sizeof(pixel), 4);
ASSERT_NE(texture, nullptr);
ResourceViewDesc viewDesc = {};
viewDesc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
viewDesc.dimension = ResourceViewDimension::Texture2D;
RHIResourceView* srv = GetDevice()->CreateShaderResourceView(texture, viewDesc);
ASSERT_NE(srv, nullptr);
EXPECT_TRUE(srv->IsValid());
EXPECT_EQ(srv->GetViewType(), ResourceViewType::ShaderResource);
srv->Shutdown();
delete srv;
texture->Shutdown();
delete texture;
}
TEST_P(RHITestFixture, Device_CreateVertexBufferView) {
BufferDesc bufferDesc = {};
bufferDesc.size = 256;

View File

@@ -65,6 +65,7 @@ cmake --build build --config Debug
cmake --build build --config Debug --target rhi_unit_tests
cmake --build build --config Debug --target rhi_integration_minimal
cmake --build build --config Debug --target rhi_integration_triangle
cmake --build build --config Debug --target rhi_integration_quad
```
### 3.3 运行
@@ -114,6 +115,7 @@ RHI 当前分成四类测试:
| 抽象层单元测试 | `rhi_unit_tests` |
| 抽象层集成测试 | `rhi_integration_minimal` |
| 抽象层集成测试 | `rhi_integration_triangle` |
| 抽象层集成测试 | `rhi_integration_quad` |
| D3D12 后端单元测试 | `rhi_d3d12_tests` |
| OpenGL 后端单元测试 | `rhi_opengl_tests` |
| D3D12 后端集成测试 | `d3d12_minimal_test` `d3d12_triangle_test` `d3d12_quad_test` `d3d12_sphere_test` |
@@ -121,8 +123,7 @@ RHI 当前分成四类测试:
说明:
- 抽象层集成测试目前正式包含 `minimal``triangle`
- `quad` 仍在向纯 RHI 抽象测试迁移中,未进入本列表。
- 抽象层集成测试目前正式包含 `minimal``triangle``quad`
## 6. RHI 抽象层集成测试规范
@@ -141,6 +142,10 @@ tests/RHI/integration/
│ ├─ CMakeLists.txt
│ ├─ GT.ppm
│ └─ main.cpp
├─ quad/
│ ├─ CMakeLists.txt
│ ├─ GT.ppm
│ └─ main.cpp
├─ compare_ppm.py
└─ CMakeLists.txt
```
@@ -157,6 +162,8 @@ tests/RHI/integration/
- `minimal_opengl.ppm`
- `triangle_d3d12.ppm`
- `triangle_opengl.ppm`
- `quad_d3d12.ppm`
- `quad_opengl.ppm`
5. 两个后端都必须与同一张 `GT.ppm` 做比对。
6. 新测试如果暴露抽象层缺口,应先补 RHI再补测试。