Files
aurora/lib/webgpu/gpu.cpp
T
Pieter-Jan Briers f61c219ef2 Add functionality to lock GX framebuffer aspect ratio (#97)
* Add functionality to lock GX framebuffer aspect ratio

This leaves the imgui (so including OS) framebuffer alone.

AuroraWindowSize now has new fields for the native framebuffer, the aspect ratio controls modify the existing fields.

* Fix from merge

* fix gx_test_stubs.cpp
2026-04-06 21:05:55 -06:00

628 lines
22 KiB
C++

#include "gpu.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <utility>
#include <vector>
#include <aurora/aurora.h>
#include <magic_enum.hpp>
#include <webgpu/webgpu.h>
#include <webgpu/webgpu_cpp.h>
#include "../internal.hpp"
#include "../window.hpp"
#ifdef WEBGPU_DAWN
#include "../dawn/BackendBinding.hpp"
#include <dawn/native/DawnNative.h>
#endif
namespace aurora::gx {
void clear_copy_texture_cache() noexcept;
} // namespace aurora::gx
namespace aurora::gfx {
void clear_offscreen_cache();
} // namespace aurora::gfx
namespace aurora::webgpu {
static Module Log("aurora::gpu");
wgpu::Device g_device;
wgpu::Queue g_queue;
wgpu::Surface g_surface;
wgpu::BackendType g_backendType;
GraphicsConfig g_graphicsConfig;
TextureWithSampler g_frameBuffer;
TextureWithSampler g_frameBufferResolved;
TextureWithSampler g_depthBuffer;
// EFB -> XFB copy pipeline
static wgpu::BindGroupLayout g_CopyBindGroupLayout;
wgpu::RenderPipeline g_CopyPipeline;
wgpu::BindGroup g_CopyBindGroup;
static wgpu::Adapter g_adapter;
wgpu::Instance g_instance;
static wgpu::AdapterInfo g_adapterInfo;
TextureWithSampler create_render_texture(uint32_t width, uint32_t height, bool multisampled) {
const wgpu::Extent3D size{
.width = width,
.height = height,
.depthOrArrayLayers = 1,
};
const auto format = g_graphicsConfig.surfaceConfiguration.format;
uint32_t sampleCount = 1;
if (multisampled) {
sampleCount = g_graphicsConfig.msaaSamples;
}
const wgpu::TextureDescriptor textureDescriptor{
.label = "Render texture",
.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc |
wgpu::TextureUsage::CopyDst,
.dimension = wgpu::TextureDimension::e2D,
.size = size,
.format = format,
.mipLevelCount = 1,
.sampleCount = sampleCount,
};
auto texture = g_device.CreateTexture(&textureDescriptor);
const wgpu::TextureViewDescriptor viewDescriptor{
.label = "Render texture view",
.dimension = wgpu::TextureViewDimension::e2D,
};
auto view = texture.CreateView(&viewDescriptor);
const wgpu::SamplerDescriptor samplerDescriptor{
.label = "Render sampler",
.addressModeU = wgpu::AddressMode::ClampToEdge,
.addressModeV = wgpu::AddressMode::ClampToEdge,
.addressModeW = wgpu::AddressMode::ClampToEdge,
.magFilter = wgpu::FilterMode::Linear,
.minFilter = wgpu::FilterMode::Linear,
.mipmapFilter = wgpu::MipmapFilterMode::Linear,
.lodMinClamp = 0.f,
.lodMaxClamp = 1000.f,
.maxAnisotropy = 1,
};
auto sampler = g_device.CreateSampler(&samplerDescriptor);
return {
.texture = std::move(texture),
.view = std::move(view),
.size = size,
.format = format,
.sampler = std::move(sampler),
};
}
static TextureWithSampler create_depth_texture(uint32_t width, uint32_t height) {
const wgpu::Extent3D size{
.width = width,
.height = height,
.depthOrArrayLayers = 1,
};
const auto format = g_graphicsConfig.depthFormat;
const wgpu::TextureDescriptor textureDescriptor{
.label = "Depth texture",
.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding,
.dimension = wgpu::TextureDimension::e2D,
.size = size,
.format = format,
.mipLevelCount = 1,
.sampleCount = g_graphicsConfig.msaaSamples,
};
auto texture = g_device.CreateTexture(&textureDescriptor);
const wgpu::TextureViewDescriptor viewDescriptor{
.label = "Depth texture view",
.dimension = wgpu::TextureViewDimension::e2D,
};
auto view = texture.CreateView(&viewDescriptor);
const wgpu::SamplerDescriptor samplerDescriptor{
.label = "Depth sampler",
.addressModeU = wgpu::AddressMode::ClampToEdge,
.addressModeV = wgpu::AddressMode::ClampToEdge,
.addressModeW = wgpu::AddressMode::ClampToEdge,
.magFilter = wgpu::FilterMode::Linear,
.minFilter = wgpu::FilterMode::Linear,
.mipmapFilter = wgpu::MipmapFilterMode::Linear,
.lodMinClamp = 0.f,
.lodMaxClamp = 1000.f,
.maxAnisotropy = 1,
};
auto sampler = g_device.CreateSampler(&samplerDescriptor);
return {
.texture = std::move(texture),
.view = std::move(view),
.size = size,
.format = format,
.sampler = std::move(sampler),
};
}
void create_copy_pipeline() {
wgpu::ShaderSourceWGSL sourceDescriptor{};
sourceDescriptor.code = R"""(
@group(0) @binding(0)
var efb_sampler: sampler;
@group(0) @binding(1)
var efb_texture: texture_2d<f32>;
struct VertexOutput {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
};
var<private> pos: array<vec2<f32>, 3> = array<vec2<f32>, 3>(
vec2(-1.0, 1.0),
vec2(-1.0, -3.0),
vec2(3.0, 1.0),
);
var<private> uvs: array<vec2<f32>, 3> = array<vec2<f32>, 3>(
vec2(0.0, 0.0),
vec2(0.0, 2.0),
vec2(2.0, 0.0),
);
@vertex
fn vs_main(@builtin(vertex_index) vtxIdx: u32) -> VertexOutput {
var out: VertexOutput;
out.pos = vec4<f32>(pos[vtxIdx], 0.0, 1.0);
out.uv = uvs[vtxIdx];
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(efb_texture, efb_sampler, in.uv);
}
)""";
const wgpu::ShaderModuleDescriptor moduleDescriptor{
.nextInChain = &sourceDescriptor,
.label = "XFB Copy Module",
};
auto module = g_device.CreateShaderModule(&moduleDescriptor);
const std::array colorTargets{wgpu::ColorTargetState{
.format = g_graphicsConfig.surfaceConfiguration.format,
.writeMask = wgpu::ColorWriteMask::All,
}};
const wgpu::FragmentState fragmentState{
.module = module,
.entryPoint = "fs_main",
.targetCount = colorTargets.size(),
.targets = colorTargets.data(),
};
const std::array bindGroupLayoutEntries{
wgpu::BindGroupLayoutEntry{
.binding = 0,
.visibility = wgpu::ShaderStage::Fragment,
.sampler =
wgpu::SamplerBindingLayout{
.type = wgpu::SamplerBindingType::Filtering,
},
},
wgpu::BindGroupLayoutEntry{
.binding = 1,
.visibility = wgpu::ShaderStage::Fragment,
.texture =
wgpu::TextureBindingLayout{
.sampleType = wgpu::TextureSampleType::Float,
.viewDimension = wgpu::TextureViewDimension::e2D,
},
},
};
const wgpu::BindGroupLayoutDescriptor bindGroupLayoutDescriptor{
.entryCount = bindGroupLayoutEntries.size(),
.entries = bindGroupLayoutEntries.data(),
};
g_CopyBindGroupLayout = g_device.CreateBindGroupLayout(&bindGroupLayoutDescriptor);
const wgpu::PipelineLayoutDescriptor layoutDescriptor{
.bindGroupLayoutCount = 1,
.bindGroupLayouts = &g_CopyBindGroupLayout,
};
auto pipelineLayout = g_device.CreatePipelineLayout(&layoutDescriptor);
const wgpu::RenderPipelineDescriptor pipelineDescriptor{
.layout = pipelineLayout,
.vertex =
wgpu::VertexState{
.module = module,
.entryPoint = "vs_main",
},
.primitive =
wgpu::PrimitiveState{
.topology = wgpu::PrimitiveTopology::TriangleList,
},
.multisample =
wgpu::MultisampleState{
.count = 1,
.mask = UINT32_MAX,
},
.fragment = &fragmentState,
};
g_CopyPipeline = g_device.CreateRenderPipeline(&pipelineDescriptor);
}
void create_copy_bind_group() {
const std::array bindGroupEntries{
wgpu::BindGroupEntry{
.binding = 0,
.sampler = g_graphicsConfig.msaaSamples > 1 ? g_frameBufferResolved.sampler : g_frameBuffer.sampler,
},
wgpu::BindGroupEntry{
.binding = 1,
.textureView = g_graphicsConfig.msaaSamples > 1 ? g_frameBufferResolved.view : g_frameBuffer.view,
},
};
const wgpu::BindGroupDescriptor bindGroupDescriptor{
.layout = g_CopyBindGroupLayout,
.entryCount = bindGroupEntries.size(),
.entries = bindGroupEntries.data(),
};
g_CopyBindGroup = g_device.CreateBindGroup(&bindGroupDescriptor);
}
static wgpu::BackendType to_wgpu_backend(AuroraBackend backend) {
switch (backend) {
case BACKEND_WEBGPU:
return wgpu::BackendType::WebGPU;
case BACKEND_D3D11:
return wgpu::BackendType::D3D11;
case BACKEND_D3D12:
return wgpu::BackendType::D3D12;
case BACKEND_METAL:
return wgpu::BackendType::Metal;
case BACKEND_VULKAN:
return wgpu::BackendType::Vulkan;
case BACKEND_OPENGL:
return wgpu::BackendType::OpenGL;
case BACKEND_OPENGLES:
return wgpu::BackendType::OpenGLES;
default:
return wgpu::BackendType::Null;
}
}
static bool create_surface() {
#ifdef EMSCRIPTEN
const WGPUSurfaceDescriptorFromCanvasHTMLSelector canvasDescriptor{
.chain = {.sType = WGPUSType_SurfaceDescriptorFromCanvasHTMLSelector},
.selector = "#canvas",
};
const wgpu::SurfaceDescriptor surfaceDescriptor{
.nextInChain = reinterpret_cast<const wgpu::ChainedStruct*>(&canvasDescriptor.chain),
.label = "Surface",
};
#else
SDL_Window* window = window::get_sdl_window();
if (window == nullptr) {
Log.error("Failed to create surface: no window");
return false;
}
const auto chainedDescriptor = utils::SetupWindowAndGetSurfaceDescriptor(window);
if (!chainedDescriptor) {
Log.error("Failed to create surface descriptor for current window");
return false;
}
const wgpu::SurfaceDescriptor surfaceDescriptor{
.nextInChain = chainedDescriptor.get(),
.label = "Surface",
};
#endif
g_surface = g_instance.CreateSurface(&surfaceDescriptor);
if (!g_surface) {
Log.error("Failed to create surface");
return false;
}
return true;
}
bool initialize(AuroraBackend auroraBackend) {
if (!g_instance) {
#ifdef WEBGPU_DAWN
Log.info("Initializing Dawn");
#endif
Log.info("Creating WGPU instance");
const std::array requiredInstanceFeatures{
wgpu::InstanceFeatureName::TimedWaitAny,
};
wgpu::InstanceDescriptor instanceDescriptor{
.requiredFeatureCount = requiredInstanceFeatures.size(),
.requiredFeatures = requiredInstanceFeatures.data(),
};
#ifdef WEBGPU_DAWN
dawn::native::DawnInstanceDescriptor dawnInstanceDescriptor;
dawnInstanceDescriptor.backendValidationLevel = dawn::native::BackendValidationLevel::Disabled;
instanceDescriptor.nextInChain = &dawnInstanceDescriptor;
#endif
g_instance = wgpu::CreateInstance(&instanceDescriptor);
if (!g_instance) {
Log.error("Failed to create WGPU instance");
return false;
}
}
const wgpu::BackendType backend = to_wgpu_backend(auroraBackend);
#ifdef EMSCRIPTEN
if (backend != wgpu::BackendType::WebGPU) {
Log.warn("Backend type {} unsupported", magic_enum::enum_name(backend));
return false;
}
#endif
Log.info("Attempting to initialize {}", magic_enum::enum_name(backend));
#if 0
// D3D12's debug layer is very slow
g_dawnInstance->EnableBackendValidation(backend != WGPUBackendType::D3D12);
#endif
if (!create_surface()) {
return false;
}
{
const wgpu::RequestAdapterOptions options{
.powerPreference = wgpu::PowerPreference::HighPerformance,
.backendType = backend,
.compatibleSurface = g_surface,
};
const auto future = g_instance.RequestAdapter(
&options, wgpu::CallbackMode::WaitAnyOnly,
[](wgpu::RequestAdapterStatus status, wgpu::Adapter adapter, wgpu::StringView message) {
if (status == wgpu::RequestAdapterStatus::Success) {
g_adapter = std::move(adapter);
} else {
Log.warn("Adapter request failed: {}", message);
}
});
const auto status = g_instance.WaitAny(future, 5000000000);
if (status != wgpu::WaitStatus::Success) {
Log.error("Failed to create adapter: {}", magic_enum::enum_name(status));
return false;
}
if (!g_adapter) {
Log.error("Failed to create adapter");
return false;
}
}
g_adapter.GetInfo(&g_adapterInfo);
g_backendType = g_adapterInfo.backendType;
const auto backendName = magic_enum::enum_name(g_backendType);
auto adapterName = g_adapterInfo.device;
if (adapterName.IsUndefined()) {
adapterName = wgpu::StringView("Unknown");
}
auto description = g_adapterInfo.description;
if (description.IsUndefined()) {
description = wgpu::StringView("Unknown");
}
Log.info("Graphics adapter information\n API: {}\n Device: {} ({})\n Driver: {}", backendName, adapterName,
magic_enum::enum_name(g_adapterInfo.adapterType), description);
{
wgpu::Limits supportedLimits{};
g_adapter.GetLimits(&supportedLimits);
const wgpu::Limits requiredLimits{
// Use "best" supported limits
.maxTextureDimension1D = supportedLimits.maxTextureDimension1D == 0 ? WGPU_LIMIT_U32_UNDEFINED
: supportedLimits.maxTextureDimension1D,
.maxTextureDimension2D = supportedLimits.maxTextureDimension2D == 0 ? WGPU_LIMIT_U32_UNDEFINED
: supportedLimits.maxTextureDimension2D,
.maxTextureDimension3D = supportedLimits.maxTextureDimension3D == 0 ? WGPU_LIMIT_U32_UNDEFINED
: supportedLimits.maxTextureDimension3D,
.maxTextureArrayLayers = supportedLimits.maxTextureArrayLayers == 0 ? WGPU_LIMIT_U32_UNDEFINED
: supportedLimits.maxTextureArrayLayers,
.maxDynamicStorageBuffersPerPipelineLayout = supportedLimits.maxDynamicStorageBuffersPerPipelineLayout == 0
? WGPU_LIMIT_U32_UNDEFINED
: supportedLimits.maxDynamicStorageBuffersPerPipelineLayout,
.maxStorageBuffersPerShaderStage = supportedLimits.maxStorageBuffersPerShaderStage == 0
? WGPU_LIMIT_U32_UNDEFINED
: supportedLimits.maxStorageBuffersPerShaderStage,
.minUniformBufferOffsetAlignment =
supportedLimits.minUniformBufferOffsetAlignment < 64 ? 64 : supportedLimits.minUniformBufferOffsetAlignment,
.minStorageBufferOffsetAlignment =
supportedLimits.minStorageBufferOffsetAlignment < 16 ? 16 : supportedLimits.minStorageBufferOffsetAlignment,
};
Log.info(
"Using limits:"
"\n maxTextureDimension1D: {}"
"\n maxTextureDimension2D: {}"
"\n maxTextureDimension3D: {}"
"\n maxTextureArrayLayers: {}"
"\n maxDynamicStorageBuffersPerPipelineLayout: {}"
"\n maxStorageBuffersPerShaderStage: {}"
"\n minUniformBufferOffsetAlignment: {}"
"\n minStorageBufferOffsetAlignment: {}",
requiredLimits.maxTextureDimension1D, requiredLimits.maxTextureDimension2D,
requiredLimits.maxTextureDimension3D, requiredLimits.maxTextureArrayLayers,
requiredLimits.maxDynamicStorageBuffersPerPipelineLayout, requiredLimits.maxStorageBuffersPerShaderStage,
requiredLimits.minUniformBufferOffsetAlignment, requiredLimits.minStorageBufferOffsetAlignment);
std::vector requiredFeatures{wgpu::FeatureName::TextureComponentSwizzle};
#ifdef WEBGPU_DAWN
const std::array enableToggles{
/* clang-format off */
#if _WIN32
"use_dxc",
#ifndef NDEBUG
"emit_hlsl_debug_symbols",
#endif
#endif
#ifdef NDEBUG
"skip_validation",
"disable_robustness",
#endif
#ifndef ANDROID
"use_user_defined_labels_in_backend",
#endif
"disable_symbol_renaming",
"enable_immediate_error_handling",
/* clang-format on */
};
const wgpu::DawnTogglesDescriptor togglesDescriptor({
.enabledToggleCount = enableToggles.size(),
.enabledToggles = enableToggles.data(),
});
#endif
wgpu::DeviceDescriptor deviceDescriptor({
#ifdef WEBGPU_DAWN
.nextInChain = &togglesDescriptor,
#endif
.requiredFeatureCount = requiredFeatures.size(),
.requiredFeatures = requiredFeatures.data(),
.requiredLimits = &requiredLimits,
});
deviceDescriptor.SetUncapturedErrorCallback(
[](const wgpu::Device& device, wgpu::ErrorType type, wgpu::StringView message) {
FATAL("WebGPU error {}: {}", underlying(type), message);
});
deviceDescriptor.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous,
[](const wgpu::Device& device, wgpu::DeviceLostReason reason,
wgpu::StringView message) { Log.warn("Device lost: {}", message); });
const auto future =
g_adapter.RequestDevice(&deviceDescriptor, wgpu::CallbackMode::WaitAnyOnly,
[](wgpu::RequestDeviceStatus status, wgpu::Device device, wgpu::StringView message) {
if (status == wgpu::RequestDeviceStatus::Success) {
g_device = std::move(device);
} else {
Log.warn("Device request failed: {}", message);
}
});
const auto status = g_instance.WaitAny(future, 5000000000);
if (status != wgpu::WaitStatus::Success) {
Log.error("Failed to create device: {}", magic_enum::enum_name(status));
return false;
}
if (!g_device) {
return false;
}
g_device.SetLoggingCallback([](wgpu::LoggingType type, wgpu::StringView message) {
AuroraLogLevel level = LOG_FATAL;
switch (type) {
case wgpu::LoggingType::Verbose:
level = LOG_DEBUG;
break;
case wgpu::LoggingType::Info:
level = LOG_INFO;
break;
case wgpu::LoggingType::Warning:
level = LOG_WARNING;
break;
case wgpu::LoggingType::Error:
level = LOG_ERROR;
break;
default:
break;
}
Log.report(level, "WebGPU message: {}", message);
});
}
g_queue = g_device.GetQueue();
wgpu::SurfaceCapabilities surfaceCapabilities;
const wgpu::Status status = g_surface.GetCapabilities(g_adapter, &surfaceCapabilities);
if (status != wgpu::Status::Success) {
Log.error("Failed to get surface capabilities: {}", magic_enum::enum_name(status));
return false;
}
if (surfaceCapabilities.formatCount == 0) {
Log.error("Surface has no formats");
return false;
}
if (surfaceCapabilities.presentModeCount == 0) {
Log.error("Surface has no present modes");
return false;
}
auto surfaceFormat = surfaceCapabilities.formats[0];
auto presentMode = surfaceCapabilities.presentModes[0];
if (surfaceFormat == wgpu::TextureFormat::RGBA8UnormSrgb) {
surfaceFormat = wgpu::TextureFormat::RGBA8Unorm;
} else if (surfaceFormat == wgpu::TextureFormat::BGRA8UnormSrgb) {
surfaceFormat = wgpu::TextureFormat::BGRA8Unorm;
}
Log.info("Using surface format {}, present mode {}", magic_enum::enum_name(surfaceFormat),
magic_enum::enum_name(presentMode));
const auto size = window::get_window_size();
g_graphicsConfig = GraphicsConfig{
.surfaceConfiguration =
wgpu::SurfaceConfiguration{
.format = surfaceFormat,
.usage = wgpu::TextureUsage::RenderAttachment,
.width = size.native_fb_width,
.height = size.native_fb_height,
.presentMode = presentMode,
},
.depthFormat = wgpu::TextureFormat::Depth32Float,
.msaaSamples = g_config.msaa,
.textureAnisotropy = g_config.maxTextureAnisotropy,
};
create_copy_pipeline();
resize_swapchain(size.fb_width, size.fb_height, size.native_fb_width, size.native_fb_width, true);
return true;
}
void shutdown() {
g_CopyBindGroupLayout = {};
g_CopyPipeline = {};
g_CopyBindGroup = {};
g_frameBuffer = {};
g_frameBufferResolved = {};
g_depthBuffer = {};
g_queue = {};
g_surface = {};
g_device = {};
g_adapter = {};
g_instance = {};
}
bool refresh_surface(bool recreate) {
if (!g_instance || !g_device) {
return false;
}
if ((!g_surface || recreate) && !create_surface()) {
return false;
}
uint32_t width = g_graphicsConfig.surfaceConfiguration.width;
uint32_t height = g_graphicsConfig.surfaceConfiguration.height;
uint32_t native_width = width;
uint32_t native_height = height;
if (window::get_sdl_window() != nullptr) {
const auto size = window::get_window_size();
width = size.fb_width;
height = size.fb_height;
native_width = size.native_fb_width;
native_height = size.native_fb_height;
}
if (width != 0 && height != 0) {
resize_swapchain(width, height, native_width, native_height, true);
}
return true;
}
void resize_swapchain(uint32_t width, uint32_t height, uint32_t native_width, uint32_t native_height, bool force) {
if (!g_surface || !g_device || width == 0 || height == 0 || native_height == 0 || native_width == 0) {
return;
}
const bool sizeChanged = g_graphicsConfig.surfaceConfiguration.width != native_width ||
g_graphicsConfig.surfaceConfiguration.height != native_height ||
g_frameBuffer.size.width != width || g_frameBuffer.size.height != height;
if (!force && !sizeChanged) {
return;
}
if (sizeChanged) {
gx::clear_copy_texture_cache();
gfx::clear_offscreen_cache();
}
g_graphicsConfig.surfaceConfiguration.width = native_width;
g_graphicsConfig.surfaceConfiguration.height = native_height;
auto surfaceConfiguration = g_graphicsConfig.surfaceConfiguration;
surfaceConfiguration.device = g_device;
g_surface.Configure(&surfaceConfiguration);
g_frameBuffer = create_render_texture(width, height, true);
g_frameBufferResolved = create_render_texture(width, height, false);
g_depthBuffer = create_depth_texture(width, height);
create_copy_bind_group();
}
} // namespace aurora::webgpu