aurora: Working movie player (again)

This commit is contained in:
Luke Street 2022-02-19 00:33:56 -05:00
parent c64f9eb2d1
commit b6b68135ef
22 changed files with 1132 additions and 387 deletions

View File

@ -1,7 +1,9 @@
#include "Runtime/CGameAllocator.hpp" #include "Runtime/CGameAllocator.hpp"
#include <logvisor/logvisor.hpp>
namespace metaforce { namespace metaforce {
logvisor::Module AllocLog("metaforce::CGameAllocator"); static logvisor::Module Log("metaforce::CGameAllocator");
#pragma GCC diagnostic ignored "-Wclass-memaccess" #pragma GCC diagnostic ignored "-Wclass-memaccess"
@ -44,7 +46,7 @@ u8* CGameAllocator::Alloc(size_t len) {
void CGameAllocator::Free(u8* ptr) { void CGameAllocator::Free(u8* ptr) {
SChunkDescription* info = reinterpret_cast<SChunkDescription*>(ptr - sizeof(SChunkDescription)); SChunkDescription* info = reinterpret_cast<SChunkDescription*>(ptr - sizeof(SChunkDescription));
if (info->magic != 0xE8E8E8E8 || info->sentinal != 0xEFEFEFEF) { if (info->magic != 0xE8E8E8E8 || info->sentinal != 0xEFEFEFEF) {
AllocLog.report(logvisor::Fatal, FMT_STRING("Invalid chunk description, memory corruption!")); Log.report(logvisor::Fatal, FMT_STRING("Invalid chunk description, memory corruption!"));
return; return;
} }

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <chrono>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <vector> #include <vector>

View File

@ -1166,18 +1166,18 @@ void ImGuiConsole::PreUpdate() {
g_StateManager->SetActiveRandomToDefault(); g_StateManager->SetActiveRandomToDefault();
} }
if (ImGui::IsKeyReleased(int(KeyCode::Grave))) { if (ImGui::IsKeyReleased(ImGuiKey_GraveAccent)) {
m_isVisible ^= 1; m_isVisible ^= 1;
} }
if (m_stepFrame) { if (m_stepFrame) {
g_Main->SetPaused(true); g_Main->SetPaused(true);
m_stepFrame = false; m_stepFrame = false;
} }
if (m_paused && !m_stepFrame && ImGui::IsKeyPressed(int(KeyCode::F6))) { if (m_paused && !m_stepFrame && ImGui::IsKeyPressed(ImGuiKey_F6)) {
g_Main->SetPaused(false); g_Main->SetPaused(false);
m_stepFrame = true; m_stepFrame = true;
} }
if (ImGui::IsKeyReleased(int(KeyCode::F5))) { if (ImGui::IsKeyReleased(ImGuiKey_F5)) {
m_paused ^= 1; m_paused ^= 1;
g_Main->SetPaused(m_paused); g_Main->SetPaused(m_paused);
} }

View File

@ -1,5 +1,7 @@
#include "Runtime/World/CPathFindArea.hpp" #include "Runtime/World/CPathFindArea.hpp"
#include <logvisor/logvisor.hpp>
#include "Runtime/CToken.hpp" #include "Runtime/CToken.hpp"
#include "Runtime/IVParamObj.hpp" #include "Runtime/IVParamObj.hpp"

View File

@ -4,6 +4,9 @@ if (NOT MSVC)
endif () endif ()
add_subdirectory(../extern/dawn dawn EXCLUDE_FROM_ALL) add_subdirectory(../extern/dawn dawn EXCLUDE_FROM_ALL)
target_compile_definitions(dawn_native PRIVATE
DAWN_ENABLE_VULKAN_VALIDATION_LAYERS
DAWN_VK_DATA_DIR="vulkandata")
if (NOT MSVC) if (NOT MSVC)
target_compile_options(SPIRV-Tools-static PRIVATE -Wno-implicit-fallthrough) target_compile_options(SPIRV-Tools-static PRIVATE -Wno-implicit-fallthrough)
target_compile_options(SPIRV-Tools-opt PRIVATE -Wno-implicit-fallthrough) target_compile_options(SPIRV-Tools-opt PRIVATE -Wno-implicit-fallthrough)
@ -11,6 +14,7 @@ endif ()
add_library(aurora STATIC add_library(aurora STATIC
lib/aurora.cpp lib/aurora.cpp
lib/gpu.cpp
lib/imgui.cpp lib/imgui.cpp
lib/dawn/BackendBinding.cpp lib/dawn/BackendBinding.cpp
lib/gfx/common.cpp lib/gfx/common.cpp

View File

@ -1,65 +1,31 @@
#include <aurora/aurora.hpp> #include <aurora/aurora.hpp>
#include "gfx/common.hpp"
#include "gpu.hpp"
#include "imgui.hpp"
#include <SDL.h> #include <SDL.h>
#include <dawn/native/DawnNative.h> #include <imgui.h>
// TODO HACK: dawn doesn't expose device toggles
#include "../extern/dawn/src/dawn/native/Toggles.h"
#include <dawn/webgpu_cpp.h>
#include <logvisor/logvisor.hpp> #include <logvisor/logvisor.hpp>
#include <magic_enum.hpp> #include <magic_enum.hpp>
#ifdef DAWN_ENABLE_BACKEND_VULKAN
#include <SDL_vulkan.h>
#include <dawn/native/VulkanBackend.h>
#endif
#ifdef DAWN_ENABLE_BACKEND_OPENGL
#include <SDL_opengl.h>
#include <dawn/native/OpenGLBackend.h>
#endif
#ifdef DAWN_ENABLE_BACKEND_METAL
#include <SDL_metal.h>
#include <dawn/native/MetalBackend.h>
#endif
#include "dawn/BackendBinding.hpp"
// imgui
#include <backends/imgui_impl_sdl.h>
#include <backends/imgui_impl_wgpu.h>
// TODO HACK: dawn doesn't expose device toggles
namespace dawn::native {
class DeviceBase {
public:
void SetToggle(Toggle toggle, bool isEnabled);
};
} // namespace dawn::native
namespace aurora { namespace aurora {
// TODO: Move global state to a class/struct?
static logvisor::Module Log("aurora"); static logvisor::Module Log("aurora");
// TODO: Move global state to a class/struct?
static std::unique_ptr<AppDelegate> g_AppDelegate; static std::unique_ptr<AppDelegate> g_AppDelegate;
static std::vector<std::string> g_Args; static std::vector<std::string> g_Args;
// SDL // SDL
static SDL_Window* g_Window; static SDL_Window* g_Window;
// Dawn / WebGPU // GPU
#ifdef DAWN_ENABLE_BACKEND_VULKAN using gpu::g_depthBuffer;
static wgpu::BackendType preferredBackendType = wgpu::BackendType::Vulkan; using gpu::g_device;
#elif DAWN_ENABLE_BACKEND_METAL using gpu::g_frameBuffer;
static wgpu::BackendType preferredBackendType = wgpu::BackendType::Metal; using gpu::g_frameBufferResolved;
#else using gpu::g_queue;
static wgpu::BackendType preferredBackendType = wgpu::BackendType::OpenGL; using gpu::g_swapChain;
#endif
static std::unique_ptr<dawn::native::Instance> g_Instance;
static dawn::native::Adapter g_Adapter;
static wgpu::AdapterProperties g_AdapterProperties;
wgpu::Device g_Device;
wgpu::Queue g_Queue;
static wgpu::SwapChain g_SwapChain;
static std::unique_ptr<utils::BackendBinding> g_BackendBinding;
static void set_window_icon(Icon icon) noexcept { static void set_window_icon(Icon icon) noexcept {
SDL_Surface* iconSurface = SDL_CreateRGBSurfaceFrom(icon.data.get(), icon.width, icon.height, 32, 4 * icon.width, SDL_Surface* iconSurface = SDL_CreateRGBSurfaceFrom(icon.data.get(), icon.width, icon.height, 32, 4 * icon.width,
@ -79,7 +45,7 @@ static void set_window_icon(Icon icon) noexcept {
static bool poll_events() noexcept { static bool poll_events() noexcept {
SDL_Event event; SDL_Event event;
while (SDL_PollEvent(&event) != 0) { while (SDL_PollEvent(&event) != 0) {
ImGui_ImplSDL2_ProcessEvent(&event); imgui::process_event(event);
switch (event.type) { switch (event.type) {
case SDL_WINDOWEVENT: { case SDL_WINDOWEVENT: {
@ -98,8 +64,7 @@ static bool poll_events() noexcept {
break; break;
} }
case SDL_WINDOWEVENT_RESIZED: { case SDL_WINDOWEVENT_RESIZED: {
auto format = static_cast<wgpu::TextureFormat>(g_BackendBinding->GetPreferredSwapChainTextureFormat()); gpu::resize_swapchain(event.window.data1, event.window.data2);
g_SwapChain.Configure(format, wgpu::TextureUsage::RenderAttachment, event.window.data1, event.window.data2);
g_AppDelegate->onAppWindowResized( g_AppDelegate->onAppWindowResized(
{static_cast<uint32_t>(event.window.data1), static_cast<uint32_t>(event.window.data2)}); {static_cast<uint32_t>(event.window.data1), static_cast<uint32_t>(event.window.data2)});
break; break;
@ -211,7 +176,7 @@ void app_run(std::unique_ptr<AppDelegate> app, Icon icon, int argc, char** argv)
} }
Uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE; Uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE;
switch (preferredBackendType) { switch (gpu::preferredBackendType) {
#ifdef DAWN_ENABLE_BACKEND_VULKAN #ifdef DAWN_ENABLE_BACKEND_VULKAN
case wgpu::BackendType::Vulkan: case wgpu::BackendType::Vulkan:
flags |= SDL_WINDOW_VULKAN; flags |= SDL_WINDOW_VULKAN;
@ -236,126 +201,104 @@ void app_run(std::unique_ptr<AppDelegate> app, Icon icon, int argc, char** argv)
} }
set_window_icon(std::move(icon)); set_window_icon(std::move(icon));
Log.report(logvisor::Info, FMT_STRING("Creating Dawn instance")); gpu::initialize(g_Window);
g_Instance = std::make_unique<dawn::native::Instance>(); gfx::construct_state();
utils::DiscoverAdapter(g_Instance.get(), g_Window, preferredBackendType);
{ imgui::create_context();
std::vector<dawn::native::Adapter> adapters = g_Instance->GetAdapters();
auto adapterIt = std::find_if(adapters.begin(), adapters.end(), [](const dawn::native::Adapter adapter) -> bool {
wgpu::AdapterProperties properties;
adapter.GetProperties(&properties);
return properties.backendType == preferredBackendType;
});
if (adapterIt == adapters.end()) {
Log.report(logvisor::Fatal, FMT_STRING("Failed to find usable graphics backend"));
}
g_Adapter = *adapterIt;
}
g_Adapter.GetProperties(&g_AdapterProperties);
const auto backendName = magic_enum::enum_name(g_AdapterProperties.backendType);
Log.report(logvisor::Info, FMT_STRING("Using {} graphics backend"), backendName);
{
const std::array<wgpu::FeatureName, 1> requiredFeatures{
wgpu::FeatureName::TextureCompressionBC,
};
const auto deviceDescriptor = wgpu::DeviceDescriptor{
.requiredFeaturesCount = requiredFeatures.size(),
.requiredFeatures = requiredFeatures.data(),
};
g_Device = wgpu::Device::Acquire(g_Adapter.CreateDevice(&deviceDescriptor));
// TODO HACK: dawn doesn't expose device toggles
static_cast<dawn::native::DeviceBase*>(static_cast<void*>(g_Device.Get()))
->SetToggle(dawn::native::Toggle::UseUserDefinedLabelsInBackend, true);
}
g_BackendBinding = std::unique_ptr<utils::BackendBinding>(
utils::CreateBinding(g_AdapterProperties.backendType, g_Window, g_Device.Get()));
if (!g_BackendBinding) {
Log.report(logvisor::Fatal, FMT_STRING("Unsupported backend {}"), backendName);
}
g_Queue = g_Device.GetQueue();
{
wgpu::SwapChainDescriptor descriptor{};
descriptor.implementation = g_BackendBinding->GetSwapChainImplementation();
g_SwapChain = g_Device.CreateSwapChain(nullptr, &descriptor);
}
{
auto size = get_window_size();
auto format = static_cast<wgpu::TextureFormat>(g_BackendBinding->GetPreferredSwapChainTextureFormat());
Log.report(logvisor::Info, FMT_STRING("Using swapchain format {}"), magic_enum::enum_name(format));
g_SwapChain.Configure(format, wgpu::TextureUsage::RenderAttachment, size.width, size.height);
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr;
g_AppDelegate->onImGuiInit(1.f); // TODO scale g_AppDelegate->onImGuiInit(1.f); // TODO scale
ImGui_ImplSDL2_InitForMetal(g_Window); imgui::initialize(g_Window);
ImGui_ImplWGPU_Init(g_Device.Get(), 1, g_BackendBinding->GetPreferredSwapChainTextureFormat());
g_AppDelegate->onImGuiAddTextures(); g_AppDelegate->onImGuiAddTextures();
g_AppDelegate->onAppLaunched(); g_AppDelegate->onAppLaunched();
g_AppDelegate->onAppWindowResized(get_window_size()); g_AppDelegate->onAppWindowResized(get_window_size());
while (poll_events()) { while (poll_events()) {
ImGui_ImplWGPU_NewFrame(); imgui::new_frame();
ImGui_ImplSDL2_NewFrame(); if (!g_AppDelegate->onAppIdle(ImGui::GetIO().DeltaTime)) {
ImGui::NewFrame(); break;
}
g_AppDelegate->onAppIdle(ImGui::GetIO().DeltaTime); const wgpu::TextureView view = g_swapChain.GetCurrentTextureView();
const wgpu::TextureView view = g_SwapChain.GetCurrentTextureView();
g_AppDelegate->onAppDraw(); g_AppDelegate->onAppDraw();
ImGui::Render();
auto encoder = g_Device.CreateCommandEncoder(); const auto encoderDescriptor = wgpu::CommandEncoderDescriptor{
.label = "Redraw encoder",
};
auto encoder = g_device.CreateCommandEncoder(&encoderDescriptor);
{ {
std::array<wgpu::RenderPassColorAttachment, 1> attachments{wgpu::RenderPassColorAttachment{ const std::array attachments{
.view = view, wgpu::RenderPassColorAttachment{
.loadOp = wgpu::LoadOp::Clear, .view = view,
.storeOp = wgpu::StoreOp::Store, // .resolveTarget = g_frameBufferResolved.view,
.clearColor = {0.f, 0.f, 0.f, 0.f}, .loadOp = wgpu::LoadOp::Clear,
}}; .storeOp = wgpu::StoreOp::Store,
.clearColor = {0.f, 0.f, 0.f, 0.f},
},
};
const auto depthStencilAttachment = wgpu::RenderPassDepthStencilAttachment{
.view = g_depthBuffer.view,
.depthLoadOp = wgpu::LoadOp::Clear,
.depthStoreOp = wgpu::StoreOp::Discard,
.clearDepth = 1.f,
.stencilLoadOp = wgpu::LoadOp::Clear,
.stencilStoreOp = wgpu::StoreOp::Discard,
};
auto renderPassDescriptor = wgpu::RenderPassDescriptor{ auto renderPassDescriptor = wgpu::RenderPassDescriptor{
.label = "Render Pass", .label = "Main render pass",
.colorAttachmentCount = attachments.size(),
.colorAttachments = attachments.data(),
.depthStencilAttachment = &depthStencilAttachment,
};
auto pass = encoder.BeginRenderPass(&renderPassDescriptor);
gfx::render(pass);
pass.End();
}
{
const std::array attachments{
wgpu::RenderPassColorAttachment{
.view = view,
.loadOp = wgpu::LoadOp::Load,
.storeOp = wgpu::StoreOp::Store,
},
};
auto renderPassDescriptor = wgpu::RenderPassDescriptor{
.label = "ImGui render pass",
.colorAttachmentCount = attachments.size(), .colorAttachmentCount = attachments.size(),
.colorAttachments = attachments.data(), .colorAttachments = attachments.data(),
}; };
auto pass = encoder.BeginRenderPass(&renderPassDescriptor); auto pass = encoder.BeginRenderPass(&renderPassDescriptor);
ImGui_ImplWGPU_RenderDrawData(ImGui::GetDrawData(), pass.Get()); imgui::render(pass);
pass.End(); pass.End();
} }
const auto buffer = encoder.Finish(); const auto buffer = encoder.Finish();
g_Queue.Submit(1, &buffer); g_queue.Submit(1, &buffer);
g_SwapChain.Present(); g_swapChain.Present();
g_AppDelegate->onAppPostDraw(); g_AppDelegate->onAppPostDraw();
ImGui::EndFrame();
} }
g_AppDelegate->onAppExiting(); g_AppDelegate->onAppExiting();
wgpuSwapChainRelease(g_SwapChain.Release()); imgui::shutdown();
wgpuQueueRelease(g_Queue.Release()); gpu::shutdown();
g_BackendBinding.reset();
wgpuDeviceRelease(g_Device.Release());
g_Instance.reset();
SDL_DestroyWindow(g_Window); SDL_DestroyWindow(g_Window);
SDL_Quit(); SDL_Quit();
} }
std::vector<std::string> get_args() noexcept { return g_Args; } std::vector<std::string> get_args() noexcept { return g_Args; }
WindowSize get_window_size() noexcept { WindowSize get_window_size() noexcept {
int width, height; int width, height;
SDL_GetWindowSize(g_Window, &width, &height); SDL_GetWindowSize(g_Window, &width, &height);
return {static_cast<uint32_t>(width), static_cast<uint32_t>(height)}; return {static_cast<uint32_t>(width), static_cast<uint32_t>(height)};
} }
void set_window_title(zstring_view title) noexcept { SDL_SetWindowTitle(g_Window, title.c_str()); } void set_window_title(zstring_view title) noexcept { SDL_SetWindowTitle(g_Window, title.c_str()); }
Backend get_backend() noexcept { Backend get_backend() noexcept {
switch (g_AdapterProperties.backendType) { switch (gpu::g_backendType) {
case wgpu::BackendType::WebGPU: case wgpu::BackendType::WebGPU:
return Backend::WebGPU; return Backend::WebGPU;
case wgpu::BackendType::D3D11: case wgpu::BackendType::D3D11:
@ -374,7 +317,9 @@ Backend get_backend() noexcept {
return Backend::Invalid; return Backend::Invalid;
} }
} }
std::string_view get_backend_string() noexcept { return magic_enum::enum_name(g_AdapterProperties.backendType); }
std::string_view get_backend_string() noexcept { return magic_enum::enum_name(gpu::g_backendType); }
void set_fullscreen(bool fullscreen) noexcept { void set_fullscreen(bool fullscreen) noexcept {
auto flags = SDL_GetWindowFlags(g_Window); auto flags = SDL_GetWindowFlags(g_Window);
if (fullscreen) { if (fullscreen) {
@ -388,12 +333,15 @@ void set_fullscreen(bool fullscreen) noexcept {
int32_t get_controller_player_index(uint32_t which) noexcept { int32_t get_controller_player_index(uint32_t which) noexcept {
return -1; // TODO return -1; // TODO
} }
void set_controller_player_index(uint32_t which, int32_t index) noexcept { void set_controller_player_index(uint32_t which, int32_t index) noexcept {
// TODO // TODO
} }
bool is_controller_gamecube(uint32_t which) noexcept { bool is_controller_gamecube(uint32_t which) noexcept {
return true; // TODO return true; // TODO
} }
std::string get_controller_name(uint32_t which) noexcept { std::string get_controller_name(uint32_t which) noexcept {
return ""; // TODO return ""; // TODO
} }

View File

@ -5,7 +5,7 @@
#include <dawn/native/OpenGLBackend.h> #include <dawn/native/OpenGLBackend.h>
#endif #endif
namespace aurora::utils { namespace aurora::gpu::utils {
#if defined(DAWN_ENABLE_BACKEND_D3D12) #if defined(DAWN_ENABLE_BACKEND_D3D12)
BackendBinding* CreateD3D12Binding(SDL_Window* window, WGPUDevice device); BackendBinding* CreateD3D12Binding(SDL_Window* window, WGPUDevice device);
@ -76,4 +76,4 @@ BackendBinding* CreateBinding(wgpu::BackendType type, SDL_Window* window, WGPUDe
} }
} }
} // namespace aurora::utils } // namespace aurora::gpu::utils

View File

@ -5,7 +5,7 @@
struct SDL_Window; struct SDL_Window;
namespace aurora::utils { namespace aurora::gpu::utils {
class BackendBinding { class BackendBinding {
public: public:
@ -24,4 +24,4 @@ protected:
void DiscoverAdapter(dawn::native::Instance* instance, SDL_Window* window, wgpu::BackendType type); void DiscoverAdapter(dawn::native::Instance* instance, SDL_Window* window, wgpu::BackendType type);
BackendBinding* CreateBinding(wgpu::BackendType type, SDL_Window* window, WGPUDevice device); BackendBinding* CreateBinding(wgpu::BackendType type, SDL_Window* window, WGPUDevice device);
} // namespace aurora::utils } // namespace aurora::gpu::utils

View File

@ -24,7 +24,7 @@ template <typename T> DawnSwapChainImplementation CreateSwapChainImplementation(
return impl; return impl;
} }
namespace aurora::utils { namespace aurora::gpu::utils {
class SwapChainImplMTL { class SwapChainImplMTL {
public: public:
using WSIContext = DawnWSIContextMetal; using WSIContext = DawnWSIContextMetal;
@ -105,4 +105,4 @@ private:
}; };
BackendBinding *CreateMetalBinding(SDL_Window *window, WGPUDevice device) { return new MetalBinding(window, device); } BackendBinding *CreateMetalBinding(SDL_Window *window, WGPUDevice device) { return new MetalBinding(window, device); }
} // namespace aurora::utils } // namespace aurora::gpu::utils

View File

@ -3,26 +3,33 @@
#include <SDL_video.h> #include <SDL_video.h>
#include <dawn/native/OpenGLBackend.h> #include <dawn/native/OpenGLBackend.h>
namespace aurora::utils { namespace aurora::gpu::utils {
class OpenGLBinding : public BackendBinding { class OpenGLBinding : public BackendBinding {
public: public:
OpenGLBinding(SDL_Window* window, WGPUDevice device) : BackendBinding(window, device) {} OpenGLBinding(SDL_Window* window, WGPUDevice device) : BackendBinding(window, device) {}
uint64_t GetSwapChainImplementation() override { uint64_t GetSwapChainImplementation() override {
if (m_swapChainImpl.userData == nullptr) { if (m_swapChainImpl.userData == nullptr) {
m_swapChainImpl = dawn::native::opengl::CreateNativeSwapChainImpl( CreateSwapChainImpl();
m_device, [](void* userdata) { SDL_GL_SwapWindow(static_cast<SDL_Window*>(userdata)); }, m_window);
} }
return reinterpret_cast<uint64_t>(&m_swapChainImpl); return reinterpret_cast<uint64_t>(&m_swapChainImpl);
} }
WGPUTextureFormat GetPreferredSwapChainTextureFormat() override { WGPUTextureFormat GetPreferredSwapChainTextureFormat() override {
if (m_swapChainImpl.userData == nullptr) {
CreateSwapChainImpl();
}
return dawn::native::opengl::GetNativeSwapChainPreferredFormat(&m_swapChainImpl); return dawn::native::opengl::GetNativeSwapChainPreferredFormat(&m_swapChainImpl);
} }
private: private:
DawnSwapChainImplementation m_swapChainImpl{}; DawnSwapChainImplementation m_swapChainImpl{};
void CreateSwapChainImpl() {
m_swapChainImpl = dawn::native::opengl::CreateNativeSwapChainImpl(
m_device, [](void* userdata) { SDL_GL_SwapWindow(static_cast<SDL_Window*>(userdata)); }, m_window);
}
}; };
BackendBinding* CreateOpenGLBinding(SDL_Window* window, WGPUDevice device) { return new OpenGLBinding(window, device); } BackendBinding* CreateOpenGLBinding(SDL_Window* window, WGPUDevice device) { return new OpenGLBinding(window, device); }
} // namespace aurora::utils } // namespace aurora::gpu::utils

View File

@ -4,30 +4,36 @@
#include <cassert> #include <cassert>
#include <dawn/native/VulkanBackend.h> #include <dawn/native/VulkanBackend.h>
namespace aurora::utils { namespace aurora::gpu::utils {
class VulkanBinding : public BackendBinding { class VulkanBinding : public BackendBinding {
public: public:
VulkanBinding(SDL_Window* window, WGPUDevice device) : BackendBinding(window, device) {} VulkanBinding(SDL_Window* window, WGPUDevice device) : BackendBinding(window, device) {}
uint64_t GetSwapChainImplementation() override { uint64_t GetSwapChainImplementation() override {
if (m_swapChainImpl.userData == nullptr) { if (m_swapChainImpl.userData == nullptr) {
VkSurfaceKHR surface = VK_NULL_HANDLE; CreateSwapChainImpl();
if (SDL_Vulkan_CreateSurface(m_window, dawn::native::vulkan::GetInstance(m_device), &surface) != SDL_TRUE) {
assert(false);
}
m_swapChainImpl = dawn::native::vulkan::CreateNativeSwapChainImpl(m_device, surface);
} }
return reinterpret_cast<uint64_t>(&m_swapChainImpl); return reinterpret_cast<uint64_t>(&m_swapChainImpl);
} }
WGPUTextureFormat GetPreferredSwapChainTextureFormat() override { WGPUTextureFormat GetPreferredSwapChainTextureFormat() override {
assert(m_swapChainImpl.userData != nullptr); if (m_swapChainImpl.userData == nullptr) {
CreateSwapChainImpl();
}
return dawn::native::vulkan::GetNativeSwapChainPreferredFormat(&m_swapChainImpl); return dawn::native::vulkan::GetNativeSwapChainPreferredFormat(&m_swapChainImpl);
} }
private: private:
DawnSwapChainImplementation m_swapChainImpl{}; DawnSwapChainImplementation m_swapChainImpl{};
void CreateSwapChainImpl() {
VkSurfaceKHR surface = VK_NULL_HANDLE;
if (SDL_Vulkan_CreateSurface(m_window, dawn::native::vulkan::GetInstance(m_device), &surface) != SDL_TRUE) {
assert(false);
}
m_swapChainImpl = dawn::native::vulkan::CreateNativeSwapChainImpl(m_device, surface);
}
}; };
BackendBinding* CreateVulkanBinding(SDL_Window* window, WGPUDevice device) { return new VulkanBinding(window, device); } BackendBinding* CreateVulkanBinding(SDL_Window* window, WGPUDevice device) { return new VulkanBinding(window, device); }
} // namespace aurora::utils } // namespace aurora::gpu::utils

View File

@ -1,10 +1,20 @@
#include "common.hpp" #include "common.hpp"
#include <magic_enum.hpp> #include "../gpu.hpp"
#include "movie_player/shader.hpp" #include "movie_player/shader.hpp"
#include <logvisor/logvisor.hpp>
#include <unordered_map>
namespace aurora::gfx { namespace aurora::gfx {
static logvisor::Module Log("aurora::gfx");
using gpu::g_device;
using gpu::g_queue;
struct ShaderState {
movie_player::State moviePlayer;
};
struct ShaderDrawCommand { struct ShaderDrawCommand {
ShaderType type; ShaderType type;
union { union {
@ -45,9 +55,46 @@ zeus::CMatrix4f g_mvInv;
zeus::CMatrix4f g_proj; zeus::CMatrix4f g_proj;
metaforce::CFogState g_fogState; metaforce::CFogState g_fogState;
std::vector<Command> g_commands; static std::mutex g_pipelineMutex;
static std::unordered_map<uint64_t, wgpu::RenderPipeline> g_pipelines;
static std::vector<PipelineCreateCommand> g_queuedPipelines;
static std::unordered_map<BindGroupRef, wgpu::BindGroup> g_cachedBindGroups;
bool get_dxt_compression_supported() noexcept { return g_Device.HasFeature(wgpu::FeatureName::TextureCompressionBC); } static ByteBuffer g_verts;
static ByteBuffer g_uniforms;
static ByteBuffer g_indices;
wgpu::Buffer g_vertexBuffer;
wgpu::Buffer g_uniformBuffer;
wgpu::Buffer g_indexBuffer;
static ShaderState g_state;
static PipelineRef g_currentPipeline;
static std::vector<Command> g_commands;
using NewPipelineCallback = std::function<wgpu::RenderPipeline()>;
static PipelineRef find_pipeline(PipelineCreateCommand command, NewPipelineCallback cb) {
const auto hash = xxh3_hash(command);
bool found;
{
std::lock_guard guard{g_pipelineMutex};
const auto ref = g_pipelines.find(hash);
found = ref != g_pipelines.end();
}
if (!found) {
// TODO another thread
wgpu::RenderPipeline pipeline = cb();
{
std::lock_guard guard{g_pipelineMutex};
g_pipelines[hash] = std::move(pipeline);
}
}
return hash;
}
static void push_draw_command(ShaderDrawCommand data) { g_commands.push_back({CommandType::Draw, {.draw = data}}); }
bool get_dxt_compression_supported() noexcept { return g_device.HasFeature(wgpu::FeatureName::TextureCompressionBC); }
void update_model_view(const zeus::CMatrix4f& mv, const zeus::CMatrix4f& mv_inv) noexcept { void update_model_view(const zeus::CMatrix4f& mv, const zeus::CMatrix4f& mv_inv) noexcept {
g_mv = mv; g_mv = mv;
@ -98,8 +145,134 @@ void queue_colored_quad(CameraFilterType filter_type, ZTest z_comparison, bool z
const zeus::CRectangle& rect, float z) noexcept { const zeus::CRectangle& rect, float z) noexcept {
// TODO // TODO
} }
void queue_movie_player(const TextureHandle& tex_y, const TextureHandle& tex_u, const TextureHandle& tex_v, void queue_movie_player(const TextureHandle& tex_y, const TextureHandle& tex_u, const TextureHandle& tex_v,
const zeus::CColor& color, float h_pad, float v_pad) noexcept { const zeus::CColor& color, float h_pad, float v_pad) noexcept {
// TODO auto data = movie_player::make_draw_data(g_state.moviePlayer, tex_y, tex_u, tex_v, color, h_pad, v_pad);
push_draw_command({.type = ShaderType::MoviePlayer, .moviePlayer = data});
}
template <>
PipelineRef pipeline_ref(movie_player::PipelineConfig config) {
return find_pipeline({.type = ShaderType::MoviePlayer, .moviePlayer = config},
[=]() { return create_pipeline(g_state.moviePlayer, config); });
}
void construct_state() {
{
const auto uniformDescriptor = wgpu::BufferDescriptor{
.label = "Shared Uniform Buffer",
.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst,
.size = 134217728, // 128mb
};
g_uniformBuffer = g_device.CreateBuffer(&uniformDescriptor);
}
{
const auto vertexDescriptor = wgpu::BufferDescriptor{
.label = "Shared Vertex Buffer",
.usage = wgpu::BufferUsage::Vertex | wgpu::BufferUsage::CopyDst,
.size = 16777216, // 16mb
};
g_vertexBuffer = g_device.CreateBuffer(&vertexDescriptor);
}
{
const auto vertexDescriptor = wgpu::BufferDescriptor{
.label = "Shared Index Buffer",
.usage = wgpu::BufferUsage::Vertex | wgpu::BufferUsage::CopyDst,
.size = 4194304, // 4mb
};
g_indexBuffer = g_device.CreateBuffer(&vertexDescriptor);
}
g_state.moviePlayer = movie_player::construct_state();
}
void render(const wgpu::RenderPassEncoder& pass) {
{
g_queue.WriteBuffer(g_vertexBuffer, 0, g_verts.data(), g_verts.size());
g_queue.WriteBuffer(g_uniformBuffer, 0, g_uniforms.data(), g_uniforms.size());
g_queue.WriteBuffer(g_indexBuffer, 0, g_indices.data(), g_indices.size());
g_verts.clear();
g_uniforms.clear();
g_indices.clear();
}
g_currentPipeline = UINT64_MAX;
for (const auto& cmd : g_commands) {
switch (cmd.type) {
case CommandType::SetViewport: {
const auto& vp = cmd.data.setViewport;
pass.SetViewport(vp.rect.position.x(), vp.rect.position.y(), vp.rect.size.x(), vp.rect.size.y(), vp.znear,
vp.zfar);
} break;
case CommandType::SetScissor: {
const auto& sc = cmd.data.setScissor;
pass.SetScissorRect(sc.x, sc.y, sc.w, sc.h);
} break;
case CommandType::Draw: {
const auto& draw = cmd.data.draw;
switch (draw.type) {
case ShaderType::Aabb:
// TODO
break;
case ShaderType::TexturedQuad:
// TODO
break;
case ShaderType::MoviePlayer:
movie_player::render(g_state.moviePlayer, draw.moviePlayer, pass);
break;
}
} break;
}
}
g_commands.clear();
}
bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass) {
if (ref == g_currentPipeline) {
return true;
}
std::lock_guard guard{g_pipelineMutex};
if (!g_pipelines.contains(ref)) {
return false;
}
pass.SetPipeline(g_pipelines[ref]);
return true;
}
static inline Range push(ByteBuffer& target, const uint8_t* data, size_t length, size_t alignment) {
size_t padding = 0;
if (alignment != 0) {
padding = alignment - length % alignment;
}
auto begin = target.size();
target.append(data, length);
if (padding > 0) {
target.append_zeroes(padding);
}
return {begin, begin + length};
}
Range push_verts(const uint8_t* data, size_t length) { return push(g_verts, data, length, 0 /* TODO? */); }
Range push_indices(const uint8_t* data, size_t length) { return push(g_indices, data, length, 0 /* TODO? */); }
Range push_uniform(const uint8_t* data, size_t length) {
wgpu::SupportedLimits limits;
g_device.GetLimits(&limits);
return push(g_uniforms, data, length, limits.limits.minUniformBufferOffsetAlignment);
}
BindGroupRef bind_group_ref(const wgpu::BindGroupDescriptor& descriptor) {
const auto id =
xxh3_hash(descriptor.entries, descriptor.entryCount * sizeof(wgpu::BindGroupEntry), xxh3_hash(descriptor));
if (!g_cachedBindGroups.contains(id)) {
g_cachedBindGroups[id] = g_device.CreateBindGroup(&descriptor);
}
return id;
}
const wgpu::BindGroup& find_bind_group(BindGroupRef id) {
if (!g_cachedBindGroups.contains(id)) {
Log.report(logvisor::Fatal, FMT_STRING("get_bind_group: failed to locate {}"), id);
}
return g_cachedBindGroups[id];
} }
} // namespace aurora::gfx } // namespace aurora::gfx

View File

@ -9,24 +9,103 @@
#include <xxh_x86dispatch.h> #include <xxh_x86dispatch.h>
#endif #endif
template <typename T> #ifndef ALIGN
XXH64_hash_t xxh3_hash(const T& input, XXH64_hash_t seed = 0) { #define ALIGN(x, a) (((x) + ((a)-1)) & ~((a)-1))
return XXH3_64bits_withSeed(&input, sizeof(T), seed); #endif
}
namespace aurora { namespace aurora {
extern wgpu::Device g_Device; static inline XXH64_hash_t xxh3_hash(const void* input, size_t len, XXH64_hash_t seed = 0) {
extern wgpu::Queue g_Queue; return XXH3_64bits_withSeed(input, len, seed);
}
template <typename T>
static inline XXH64_hash_t xxh3_hash(const T& input, XXH64_hash_t seed = 0) {
return xxh3_hash(&input, sizeof(T), seed);
}
class ByteBuffer {
public:
ByteBuffer() = default;
explicit ByteBuffer(size_t capacity) : m_data(static_cast<uint8_t*>(calloc(1, capacity))), m_capacity(capacity) {}
~ByteBuffer() {
if (m_data != nullptr) {
free(m_data);
}
}
uint8_t* data() { return m_data; }
const uint8_t* data() const { return m_data; }
size_t size() const { return m_length; }
void append(const void* data, size_t size) {
resize(m_length + size);
memcpy(m_data + m_length, data, size);
m_length += size;
}
void append_zeroes(size_t size) {
resize(m_length + size);
memset(m_data + m_length, 0, size);
m_length += size;
}
void resize(size_t size) {
if (size == 0) {
clear();
} else if (m_data == nullptr) {
m_data = static_cast<uint8_t*>(malloc(size));
} else if (size > m_capacity) {
m_data = static_cast<uint8_t*>(realloc(m_data, size));
} else {
return;
}
m_capacity = size;
}
void clear() {
if (m_data != nullptr) {
free(m_data);
}
m_data = nullptr;
m_length = 0;
m_capacity = 0;
}
private:
uint8_t* m_data = nullptr;
size_t m_length = 0;
size_t m_capacity = 0;
};
template <typename T>
struct Vec2 {
T x{};
T y{};
};
template <typename T>
struct Vec3 {
T x{};
T y{};
T z{};
};
template <typename T>
struct Vec4 {
T x{};
T y{};
T z{};
T w{};
};
} // namespace aurora } // namespace aurora
namespace aurora::gfx { namespace aurora::gfx {
static logvisor::Module Log("aurora::gfx");
extern zeus::CMatrix4f g_mv; extern zeus::CMatrix4f g_mv;
extern zeus::CMatrix4f g_mvInv; extern zeus::CMatrix4f g_mvInv;
extern zeus::CMatrix4f g_proj; extern zeus::CMatrix4f g_proj;
extern metaforce::CFogState g_fogState; extern metaforce::CFogState g_fogState;
extern wgpu::Buffer g_vertexBuffer;
extern wgpu::Buffer g_uniformBuffer;
extern wgpu::Buffer g_indexBuffer;
struct TextureRef { struct TextureRef {
wgpu::Texture texture; wgpu::Texture texture;
wgpu::TextureView view; wgpu::TextureView view;
@ -38,11 +117,41 @@ struct TextureRef {
}; };
using PipelineRef = uint64_t; using PipelineRef = uint64_t;
using Range = std::pair<uint64_t, uint64_t>; using BindGroupRef = uint64_t;
using Range = std::pair<uint32_t, uint32_t>;
enum class ShaderType { enum class ShaderType {
Aabb, Aabb,
TexturedQuad, TexturedQuad,
MoviePlayer, MoviePlayer,
}; };
void construct_state();
void render(const wgpu::RenderPassEncoder& pass);
Range push_verts(const uint8_t* data, size_t length);
template <typename T>
static inline Range push_verts(ArrayRef<T> data) {
return push_verts(reinterpret_cast<const uint8_t*>(data.data()), data.size() * sizeof(T));
}
Range push_indices(const uint8_t* data, size_t length);
template <typename T>
static inline Range push_indices(ArrayRef<T> data) {
return push_indices(reinterpret_cast<const uint8_t*>(data.data()), data.size() * sizeof(T));
}
Range push_uniform(const uint8_t* data, size_t length);
template <typename T>
static inline Range push_uniform(const T& data) {
return push_uniform(reinterpret_cast<const uint8_t*>(&data), sizeof(T));
}
template <typename PipelineConfig>
PipelineRef pipeline_ref(PipelineConfig config);
bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass);
BindGroupRef bind_group_ref(const wgpu::BindGroupDescriptor& descriptor);
const wgpu::BindGroup& find_bind_group(BindGroupRef id);
static inline zeus::CMatrix4f get_combined_matrix() { return g_proj * g_mv; }
} // namespace aurora::gfx } // namespace aurora::gfx

View File

@ -0,0 +1,280 @@
#include "shader.hpp"
#include "../../gpu.hpp"
namespace aurora::gfx::movie_player {
using gpu::g_device;
using gpu::g_graphicsConfig;
using gpu::utils::make_vertex_attributes;
using gpu::utils::make_vertex_buffer_layout;
using gpu::utils::make_vertex_state;
State construct_state() {
wgpu::ShaderModuleWGSLDescriptor wgslDescriptor{};
wgslDescriptor.source = R"""(
struct Uniform {
xf: mat4x4<f32>;
color: vec4<f32>;
};
@group(0) @binding(0)
var<uniform> ubuf: Uniform;
@group(0) @binding(1)
var tex_sampler: sampler;
@group(1) @binding(0)
var tex_y: texture_2d<f32>;
@group(1) @binding(1)
var tex_u: texture_2d<f32>;
@group(1) @binding(2)
var tex_v: texture_2d<f32>;
struct VertexOutput {
@builtin(position) pos: vec4<f32>;
@location(0) uv: vec2<f32>;
};
@stage(vertex)
fn vs_main(@location(0) in_pos: vec3<f32>, @location(1) in_uv: vec2<f32>) -> VertexOutput {
var out: VertexOutput;
out.pos = ubuf.xf * vec4<f32>(in_pos, 1.0);
out.uv = in_uv;
return out;
}
@stage(fragment)
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
var yuv = vec3<f32>(
1.1643 * (textureSample(tex_y, tex_sampler, in.uv).x - 0.0625),
textureSample(tex_u, tex_sampler, in.uv).x - 0.5,
textureSample(tex_v, tex_sampler, in.uv).x - 0.5
);
return ubuf.color * vec4<f32>(
yuv.x + 1.5958 * yuv.z,
yuv.x - 0.39173 * yuv.y - 0.8129 * yuv.z,
yuv.x + 2.017 * yuv.y,
1.0
);
}
)""";
const auto shaderDescriptor = wgpu::ShaderModuleDescriptor{
.nextInChain = &wgslDescriptor,
.label = "Movie Player Shader",
};
auto shader = g_device.CreateShaderModule(&shaderDescriptor);
wgpu::SupportedLimits limits;
g_device.GetLimits(&limits);
const auto uniform_alignment = limits.limits.minUniformBufferOffsetAlignment;
const auto uniform_size = ALIGN(sizeof(Uniform), uniform_alignment);
const std::array uniformLayoutEntries{
wgpu::BindGroupLayoutEntry{
.binding = 0,
.visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment,
.buffer =
wgpu::BufferBindingLayout{
.type = wgpu::BufferBindingType::Uniform,
.hasDynamicOffset = true,
.minBindingSize = uniform_size,
},
},
wgpu::BindGroupLayoutEntry{
.binding = 1,
.visibility = wgpu::ShaderStage::Fragment,
.sampler =
wgpu::SamplerBindingLayout{
.type = wgpu::SamplerBindingType::Filtering,
},
},
};
const auto uniformLayoutDescriptor = wgpu::BindGroupLayoutDescriptor{
.label = "Movie Player Uniform Bind Group Layout",
.entryCount = uniformLayoutEntries.size(),
.entries = uniformLayoutEntries.data(),
};
auto uniformLayout = g_device.CreateBindGroupLayout(&uniformLayoutDescriptor);
const auto samplerDescriptor = wgpu::SamplerDescriptor{
.addressModeU = wgpu::AddressMode::Repeat,
.addressModeV = wgpu::AddressMode::Repeat,
.addressModeW = wgpu::AddressMode::Repeat,
.magFilter = wgpu::FilterMode::Linear,
.minFilter = wgpu::FilterMode::Linear,
.mipmapFilter = wgpu::FilterMode::Linear,
.maxAnisotropy = g_graphicsConfig.textureAnistropy,
};
auto sampler = g_device.CreateSampler(&samplerDescriptor);
const std::array uniformBindGroupEntries{
wgpu::BindGroupEntry{
.binding = 0,
.buffer = g_uniformBuffer,
.offset = 0,
.size = uniform_size,
},
wgpu::BindGroupEntry{
.binding = 1,
.sampler = sampler,
},
};
const auto uniformBindGroupDescriptor = wgpu::BindGroupDescriptor{
.label = "Movie Player Uniform Bind Group",
.layout = uniformLayout,
.entryCount = uniformBindGroupEntries.size(),
.entries = uniformBindGroupEntries.data(),
};
auto uniformBindGroup = g_device.CreateBindGroup(&uniformBindGroupDescriptor);
const auto textureBinding = wgpu::TextureBindingLayout{
.sampleType = wgpu::TextureSampleType::Float,
.viewDimension = wgpu::TextureViewDimension::e2D,
};
const std::array textureLayoutEntries{
wgpu::BindGroupLayoutEntry{
.binding = 0,
.visibility = wgpu::ShaderStage::Fragment,
.texture = textureBinding,
},
wgpu::BindGroupLayoutEntry{
.binding = 1,
.visibility = wgpu::ShaderStage::Fragment,
.texture = textureBinding,
},
wgpu::BindGroupLayoutEntry{
.binding = 2,
.visibility = wgpu::ShaderStage::Fragment,
.texture = textureBinding,
},
};
const auto textureLayoutDescriptor = wgpu::BindGroupLayoutDescriptor{
.label = "Movie Player Texture Bind Group Layout",
.entryCount = textureLayoutEntries.size(),
.entries = textureLayoutEntries.data(),
};
auto textureLayout = g_device.CreateBindGroupLayout(&textureLayoutDescriptor);
const std::array bindGroupLayouts{
uniformLayout,
textureLayout,
};
const auto pipelineLayoutDescriptor = wgpu::PipelineLayoutDescriptor{
.label = "Movie Player Pipeline Layout",
.bindGroupLayoutCount = bindGroupLayouts.size(),
.bindGroupLayouts = bindGroupLayouts.data(),
};
auto pipelineLayout = g_device.CreatePipelineLayout(&pipelineLayoutDescriptor);
return {
.shader = shader,
.uniformLayout = uniformLayout,
.uniformBindGroup = uniformBindGroup,
.textureLayout = textureLayout,
.sampler = sampler,
.pipelineLayout = pipelineLayout,
};
}
wgpu::RenderPipeline create_pipeline(const State& state, [[maybe_unused]] PipelineConfig config) {
const auto attributes =
make_vertex_attributes(std::array{wgpu::VertexFormat::Float32x3, wgpu::VertexFormat::Float32x2});
const std::array vertexBuffers{make_vertex_buffer_layout(sizeof(Vert), attributes)};
const auto depthStencil = wgpu::DepthStencilState{
.format = g_graphicsConfig.depthFormat,
};
const auto blendComponent = wgpu::BlendComponent{
.srcFactor = wgpu::BlendFactor::SrcAlpha,
.dstFactor = wgpu::BlendFactor::OneMinusSrcAlpha,
};
const auto blendState = wgpu::BlendState{
.color = blendComponent,
.alpha = blendComponent,
};
const std::array colorTargets{
wgpu::ColorTargetState{
.format = g_graphicsConfig.colorFormat,
.blend = &blendState,
.writeMask = wgpu::ColorWriteMask::Red | wgpu::ColorWriteMask::Green | wgpu::ColorWriteMask::Blue,
},
};
const auto fragmentState = wgpu::FragmentState{
.module = state.shader,
.entryPoint = "fs_main",
.targetCount = colorTargets.size(),
.targets = colorTargets.data(),
};
const auto pipelineDescriptor = wgpu::RenderPipelineDescriptor{
.label = "Movie Player Pipeline",
.layout = state.pipelineLayout,
.vertex = make_vertex_state(state.shader, vertexBuffers),
.primitive =
wgpu::PrimitiveState{
.topology = wgpu::PrimitiveTopology::TriangleStrip,
},
.depthStencil = &depthStencil,
.multisample =
wgpu::MultisampleState{
.count = g_graphicsConfig.msaaSamples,
},
.fragment = &fragmentState,
};
return g_device.CreateRenderPipeline(&pipelineDescriptor);
}
DrawData make_draw_data(const State& state, const TextureHandle& tex_y, const TextureHandle& tex_u,
const TextureHandle& tex_v, const zeus::CColor& color, float h_pad, float v_pad) {
auto pipeline = pipeline_ref(PipelineConfig{});
const std::array<Vert, 4> verts{
Vert{{-h_pad, v_pad, 0.f}, {0.0, 0.0}},
Vert{{-h_pad, -v_pad, 0.f}, {0.0, 1.0}},
Vert{{h_pad, v_pad, 0.f}, {1.0, 0.0}},
Vert{{h_pad, -v_pad, 0.f}, {1.0, 1.0}},
};
const auto vertRange = push_verts(ArrayRef{verts});
const auto uniform = Uniform{
.xf = zeus::CMatrix4f{},
.color = color,
};
const auto uniformRange = push_uniform(uniform);
std::array<wgpu::BindGroupEntry, 3> entries{
wgpu::BindGroupEntry{
.binding = 0,
.textureView = tex_y.ref->view,
},
wgpu::BindGroupEntry{
.binding = 1,
.textureView = tex_u.ref->view,
},
wgpu::BindGroupEntry{
.binding = 2,
.textureView = tex_v.ref->view,
},
};
const auto textureBindGroup = bind_group_ref(wgpu::BindGroupDescriptor{
.label = "Movie Player Texture Bind Group",
.layout = state.textureLayout,
.entryCount = entries.size(),
.entries = entries.data(),
});
return {
.pipeline = pipeline,
.vertRange = vertRange,
.uniformRange = uniformRange,
.textureBindGroup = textureBindGroup,
};
}
void render(const State& state, const DrawData& data, const wgpu::RenderPassEncoder& pass) {
if (!bind_pipeline(data.pipeline, pass)) {
return;
}
const std::array offsets{data.uniformRange.first};
pass.SetBindGroup(0, state.uniformBindGroup, offsets.size(), offsets.data());
pass.SetBindGroup(1, find_bind_group(data.textureBindGroup));
pass.SetVertexBuffer(0, g_vertexBuffer, data.vertRange.first, data.vertRange.second);
pass.Draw(4);
}
} // namespace aurora::gfx::movie_player

View File

@ -5,13 +5,13 @@ struct DrawData {
PipelineRef pipeline; PipelineRef pipeline;
Range vertRange; Range vertRange;
Range uniformRange; Range uniformRange;
uint64_t bindGroupId; BindGroupRef textureBindGroup;
}; };
struct PipelineConfig { struct PipelineConfig {
// nothing // nothing
}; };
const std::array INITIAL_PIPELINES{ static const std::array INITIAL_PIPELINES{
PipelineConfig{}, PipelineConfig{},
}; };
@ -22,8 +22,20 @@ struct State {
wgpu::BindGroupLayout textureLayout; wgpu::BindGroupLayout textureLayout;
wgpu::Sampler sampler; wgpu::Sampler sampler;
wgpu::PipelineLayout pipelineLayout; wgpu::PipelineLayout pipelineLayout;
// Transient state
std::unordered_map<uint64_t, wgpu::BindGroup> textureBindGroups;
std::vector<uint64_t> frameUsedTextures;
}; };
struct Vert {
Vec3<float> pos;
Vec2<float> uv;
};
struct Uniform {
zeus::CMatrix4f xf;
zeus::CColor color;
};
State construct_state();
wgpu::RenderPipeline create_pipeline(const State& state, [[maybe_unused]] PipelineConfig config);
DrawData make_draw_data(const State& state, const TextureHandle& tex_y, const TextureHandle& tex_u,
const TextureHandle& tex_v, const zeus::CColor& color, float h_pad, float v_pad);
void render(const State& state, const DrawData& data, const wgpu::RenderPassEncoder& pass);
} // namespace aurora::gfx::movie_player } // namespace aurora::gfx::movie_player

View File

@ -1,8 +1,15 @@
#include "common.hpp" #include "common.hpp"
#include "../gpu.hpp"
#include <magic_enum.hpp> #include <magic_enum.hpp>
namespace aurora::gfx { namespace aurora::gfx {
static logvisor::Module Log("aurora::gfx");
using gpu::g_device;
using gpu::g_queue;
static wgpu::TextureFormat to_wgpu(TextureFormat format) { static wgpu::TextureFormat to_wgpu(TextureFormat format) {
switch (format) { switch (format) {
case TextureFormat::RGBA8: case TextureFormat::RGBA8:
@ -77,7 +84,7 @@ TextureHandle new_static_texture_2d(uint32_t width, uint32_t height, uint32_t mi
.bytesPerRow = bytesPerRow, .bytesPerRow = bytesPerRow,
.rowsPerImage = heightBlocks, .rowsPerImage = heightBlocks,
}; };
g_Queue.WriteTexture(&dstView, data.data() + offset, dataSize, &dataLayout, &physicalSize); g_queue.WriteTexture(&dstView, data.data() + offset, dataSize, &dataLayout, &physicalSize);
offset += dataSize; offset += dataSize;
} }
if (offset < data.size()) { if (offset < data.size()) {
@ -108,7 +115,7 @@ TextureHandle new_dynamic_texture_2d(uint32_t width, uint32_t height, uint32_t m
.dimension = wgpu::TextureViewDimension::e2D, .dimension = wgpu::TextureViewDimension::e2D,
.mipLevelCount = mips, .mipLevelCount = mips,
}; };
auto texture = g_Device.CreateTexture(&textureDescriptor); auto texture = g_device.CreateTexture(&textureDescriptor);
auto textureView = texture.CreateView(&textureViewDescriptor); auto textureView = texture.CreateView(&textureViewDescriptor);
return {std::make_shared<TextureRef>(std::move(texture), std::move(textureView), size, wgpuFormat)}; return {std::make_shared<TextureRef>(std::move(texture), std::move(textureView), size, wgpuFormat)};
} }
@ -142,6 +149,6 @@ void write_texture(const TextureHandle& handle, ArrayRef<uint8_t> data) noexcept
.bytesPerRow = bytesPerRow, .bytesPerRow = bytesPerRow,
.rowsPerImage = heightBlocks, .rowsPerImage = heightBlocks,
}; };
g_Queue.WriteTexture(&dstView, data.data(), dataSize, &dataLayout, &ref.size); g_queue.WriteTexture(&dstView, data.data(), dataSize, &dataLayout, &ref.size);
} }
} // namespace aurora::gfx } // namespace aurora::gfx

208
aurora/lib/gpu.cpp Normal file
View File

@ -0,0 +1,208 @@
#include "gpu.hpp"
#include <dawn/native/DawnNative.h>
#include <dawn/webgpu_cpp.h>
#include <logvisor/logvisor.hpp>
#include <magic_enum.hpp>
#include <SDL.h>
#include <memory>
#include "dawn/BackendBinding.hpp"
// TODO HACK: dawn doesn't expose device toggles
#include "../extern/dawn/src/dawn/native/Toggles.h"
namespace dawn::native {
class DeviceBase {
public:
void SetToggle(Toggle toggle, bool isEnabled);
};
} // namespace dawn::native
namespace aurora::gpu {
static logvisor::Module Log("aurora::gpu");
wgpu::Device g_device;
wgpu::Queue g_queue;
wgpu::SwapChain g_swapChain;
wgpu::BackendType g_backendType;
GraphicsConfig g_graphicsConfig;
TextureWithSampler g_frameBuffer;
TextureWithSampler g_frameBufferResolved;
TextureWithSampler g_depthBuffer;
static std::unique_ptr<dawn::native::Instance> g_Instance;
static dawn::native::Adapter g_Adapter;
static wgpu::AdapterProperties g_AdapterProperties;
static std::unique_ptr<utils::BackendBinding> g_BackendBinding;
static TextureWithSampler create_render_texture(bool multisampled) {
const auto size = wgpu::Extent3D{
.width = g_graphicsConfig.width,
.height = g_graphicsConfig.height,
};
const auto format = g_graphicsConfig.colorFormat;
uint32_t sampleCount = 1;
if (multisampled) {
sampleCount = g_graphicsConfig.msaaSamples;
}
const auto textureDescriptor = wgpu::TextureDescriptor{
.label = "Render texture",
.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding,
.size = size,
.format = format,
.sampleCount = sampleCount,
};
auto texture = g_device.CreateTexture(&textureDescriptor);
const auto viewDescriptor = wgpu::TextureViewDescriptor{};
auto view = texture.CreateView(&viewDescriptor);
const auto samplerDescriptor = wgpu::SamplerDescriptor{
.label = "Render sampler",
.magFilter = wgpu::FilterMode::Linear,
.minFilter = wgpu::FilterMode::Linear,
.mipmapFilter = wgpu::FilterMode::Linear,
};
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() {
const auto size = wgpu::Extent3D{
.width = g_graphicsConfig.width,
.height = g_graphicsConfig.height,
};
const auto format = g_graphicsConfig.depthFormat;
const auto textureDescriptor = wgpu::TextureDescriptor{
.label = "Depth texture",
.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding,
.size = size,
.format = format,
.sampleCount = g_graphicsConfig.msaaSamples,
};
auto texture = g_device.CreateTexture(&textureDescriptor);
const auto viewDescriptor = wgpu::TextureViewDescriptor{};
auto view = texture.CreateView(&viewDescriptor);
const auto samplerDescriptor = wgpu::SamplerDescriptor{
.label = "Depth sampler",
.magFilter = wgpu::FilterMode::Linear,
.minFilter = wgpu::FilterMode::Linear,
.mipmapFilter = wgpu::FilterMode::Linear,
};
auto sampler = g_device.CreateSampler(&samplerDescriptor);
return {
.texture = std::move(texture),
.view = std::move(view),
.size = size,
.format = format,
.sampler = std::move(sampler),
};
}
static void error_callback(WGPUErrorType type, char const* message, void* userdata) {
Log.report(logvisor::Error, FMT_STRING("Dawn error {}: {}"),
magic_enum::enum_name(static_cast<wgpu::ErrorType>(type)), message);
}
void initialize(SDL_Window* window) {
Log.report(logvisor::Info, FMT_STRING("Creating Dawn instance"));
g_Instance = std::make_unique<dawn::native::Instance>();
g_Instance->EnableBackendValidation(true);
utils::DiscoverAdapter(g_Instance.get(), window, preferredBackendType);
{
std::vector<dawn::native::Adapter> adapters = g_Instance->GetAdapters();
auto adapterIt = std::find_if(adapters.begin(), adapters.end(), [](const dawn::native::Adapter adapter) -> bool {
wgpu::AdapterProperties properties;
adapter.GetProperties(&properties);
return properties.backendType == preferredBackendType;
});
if (adapterIt == adapters.end()) {
Log.report(logvisor::Fatal, FMT_STRING("Failed to find usable graphics backend"));
}
g_Adapter = *adapterIt;
}
g_Adapter.GetProperties(&g_AdapterProperties);
g_backendType = g_AdapterProperties.backendType;
const auto backendName = magic_enum::enum_name(g_backendType);
Log.report(logvisor::Info, FMT_STRING("Using {} graphics backend"), backendName);
{
const std::array<wgpu::FeatureName, 1> requiredFeatures{
wgpu::FeatureName::TextureCompressionBC,
};
const auto deviceDescriptor = wgpu::DeviceDescriptor{
.requiredFeaturesCount = requiredFeatures.size(),
.requiredFeatures = requiredFeatures.data(),
};
g_device = wgpu::Device::Acquire(g_Adapter.CreateDevice(&deviceDescriptor));
g_device.SetUncapturedErrorCallback(&error_callback, nullptr);
// TODO HACK: dawn doesn't expose device toggles
static_cast<dawn::native::DeviceBase*>(static_cast<void*>(g_device.Get()))
->SetToggle(dawn::native::Toggle::UseUserDefinedLabelsInBackend, true);
}
g_queue = g_device.GetQueue();
g_BackendBinding =
std::unique_ptr<utils::BackendBinding>(utils::CreateBinding(g_backendType, window, g_device.Get()));
if (!g_BackendBinding) {
Log.report(logvisor::Fatal, FMT_STRING("Unsupported backend {}"), backendName);
}
auto swapChainFormat = static_cast<wgpu::TextureFormat>(g_BackendBinding->GetPreferredSwapChainTextureFormat());
if (swapChainFormat == wgpu::TextureFormat::RGBA8UnormSrgb) {
swapChainFormat = wgpu::TextureFormat::RGBA8Unorm;
} else if (swapChainFormat == wgpu::TextureFormat::BGRA8UnormSrgb) {
swapChainFormat = wgpu::TextureFormat::BGRA8Unorm;
}
Log.report(logvisor::Info, FMT_STRING("Using swapchain swapChainFormat {}"), magic_enum::enum_name(swapChainFormat));
{
const auto descriptor = wgpu::SwapChainDescriptor{
.format = swapChainFormat,
.implementation = g_BackendBinding->GetSwapChainImplementation(),
};
g_swapChain = g_device.CreateSwapChain(nullptr, &descriptor);
}
{
int width = 0;
int height = 0;
SDL_GetWindowSize(window, &width, &height);
g_graphicsConfig = GraphicsConfig{
.width = static_cast<uint32_t>(width),
.height = static_cast<uint32_t>(height),
.colorFormat = swapChainFormat,
.depthFormat = wgpu::TextureFormat::Depth32Float,
.msaaSamples = 1, // TODO 4
.textureAnistropy = 16,
};
resize_swapchain(width, height);
}
}
void shutdown() {
wgpuSwapChainRelease(g_swapChain.Release());
wgpuQueueRelease(g_queue.Release());
g_BackendBinding.reset();
wgpuDeviceRelease(g_device.Release());
g_Instance.reset();
}
void resize_swapchain(uint32_t width, uint32_t height) {
g_graphicsConfig.width = width;
g_graphicsConfig.height = height;
g_swapChain.Configure(g_graphicsConfig.colorFormat, wgpu::TextureUsage::RenderAttachment, width, height);
g_frameBuffer = create_render_texture(true);
g_frameBufferResolved = create_render_texture(false);
g_depthBuffer = create_depth_texture();
}
} // namespace aurora::gpu

131
aurora/lib/gpu.hpp Normal file
View File

@ -0,0 +1,131 @@
#pragma once
#include <array>
#include <cstdint>
#include <dawn/webgpu_cpp.h>
struct SDL_Window;
namespace aurora::gpu {
struct GraphicsConfig {
uint32_t width;
uint32_t height;
wgpu::TextureFormat colorFormat;
wgpu::TextureFormat depthFormat;
uint32_t msaaSamples;
uint8_t textureAnistropy;
};
struct TextureWithSampler {
wgpu::Texture texture;
wgpu::TextureView view;
wgpu::Extent3D size;
wgpu::TextureFormat format;
wgpu::Sampler sampler;
};
#ifdef DAWN_ENABLE_BACKEND_VULKAN
static const wgpu::BackendType preferredBackendType = wgpu::BackendType::Vulkan;
#elif DAWN_ENABLE_BACKEND_METAL
static const wgpu::BackendType preferredBackendType = wgpu::BackendType::Metal;
#else
static const wgpu::BackendType preferredBackendType = wgpu::BackendType::OpenGL;
#endif
extern wgpu::Device g_device;
extern wgpu::Queue g_queue;
extern wgpu::SwapChain g_swapChain;
extern wgpu::BackendType g_backendType;
extern GraphicsConfig g_graphicsConfig;
extern TextureWithSampler g_frameBuffer;
extern TextureWithSampler g_frameBufferResolved;
extern TextureWithSampler g_depthBuffer;
void initialize(SDL_Window* window);
void shutdown();
void resize_swapchain(uint32_t width, uint32_t height);
} // namespace aurora::gpu
namespace aurora::gpu::utils {
template <auto N>
static consteval std::array<wgpu::VertexAttribute, N>
make_vertex_attributes(std::array<wgpu::VertexFormat, N> formats) {
std::array<wgpu::VertexAttribute, N> attributes{};
uint64_t offset = 0;
for (uint32_t i = 0; i < N; ++i) {
auto format = formats[i];
attributes[i] = wgpu::VertexAttribute{
.format = format,
.offset = offset,
.shaderLocation = i,
};
switch (format) {
case wgpu::VertexFormat::Uint8x2:
case wgpu::VertexFormat::Sint8x2:
case wgpu::VertexFormat::Unorm8x2:
case wgpu::VertexFormat::Snorm8x2:
offset += 2;
break;
case wgpu::VertexFormat::Uint8x4:
case wgpu::VertexFormat::Sint8x4:
case wgpu::VertexFormat::Unorm8x4:
case wgpu::VertexFormat::Snorm8x4:
case wgpu::VertexFormat::Uint16x2:
case wgpu::VertexFormat::Sint16x2:
case wgpu::VertexFormat::Unorm16x2:
case wgpu::VertexFormat::Snorm16x2:
case wgpu::VertexFormat::Float16x2:
case wgpu::VertexFormat::Float32:
case wgpu::VertexFormat::Uint32:
case wgpu::VertexFormat::Sint32:
offset += 4;
break;
case wgpu::VertexFormat::Uint16x4:
case wgpu::VertexFormat::Sint16x4:
case wgpu::VertexFormat::Unorm16x4:
case wgpu::VertexFormat::Snorm16x4:
case wgpu::VertexFormat::Float16x4:
case wgpu::VertexFormat::Float32x2:
case wgpu::VertexFormat::Uint32x2:
case wgpu::VertexFormat::Sint32x2:
offset += 8;
break;
case wgpu::VertexFormat::Float32x3:
case wgpu::VertexFormat::Uint32x3:
case wgpu::VertexFormat::Sint32x3:
offset += 12;
break;
case wgpu::VertexFormat::Float32x4:
case wgpu::VertexFormat::Uint32x4:
case wgpu::VertexFormat::Sint32x4:
offset += 16;
break;
case wgpu::VertexFormat::Undefined:
break;
}
}
return attributes;
}
template <auto N>
static inline wgpu::VertexBufferLayout
make_vertex_buffer_layout(uint64_t arrayStride, const std::array<wgpu::VertexAttribute, N>& attributes,
wgpu::VertexStepMode stepMode = wgpu::VertexStepMode::Vertex) {
return {
.arrayStride = arrayStride,
.stepMode = stepMode,
.attributeCount = static_cast<uint32_t>(attributes.size()),
.attributes = attributes.data(),
};
}
template <auto N>
static inline wgpu::VertexState make_vertex_state(const wgpu::ShaderModule& module,
const std::array<wgpu::VertexBufferLayout, N>& buffers,
const char* entryPoint = "vs_main") {
return {
.module = module,
.entryPoint = entryPoint,
.bufferCount = static_cast<uint32_t>(buffers.size()),
.buffers = buffers.data(),
};
}
} // namespace aurora::gpu::utils

View File

@ -1,13 +1,49 @@
#include <aurora/imgui.hpp> #include "imgui.hpp"
#include "gpu.hpp"
#include <aurora/imgui.hpp>
#include <backends/imgui_impl_sdl.h>
#include <backends/imgui_impl_wgpu.h>
#include <dawn/webgpu_cpp.h> #include <dawn/webgpu_cpp.h>
namespace aurora {
extern wgpu::Device g_Device;
extern wgpu::Queue g_Queue;
} // namespace aurora
namespace aurora::imgui { namespace aurora::imgui {
using gpu::g_device;
using gpu::g_queue;
void create_context() noexcept {
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr;
}
void initialize(SDL_Window* window) noexcept {
// this just passes through to ImGui_ImplSDL2_Init (private)
// may need to change in the future
ImGui_ImplSDL2_InitForMetal(window);
ImGui_ImplWGPU_Init(g_device.Get(), 1, static_cast<WGPUTextureFormat>(gpu::g_graphicsConfig.colorFormat));
}
void shutdown() noexcept {
ImGui_ImplWGPU_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
}
void process_event(const SDL_Event& event) noexcept { ImGui_ImplSDL2_ProcessEvent(&event); }
void new_frame() noexcept {
ImGui_ImplWGPU_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
}
void render(const wgpu::RenderPassEncoder& pass) noexcept {
ImGui::Render();
ImGui_ImplWGPU_RenderDrawData(ImGui::GetDrawData(), pass.Get());
}
ImTextureID add_texture(uint32_t width, uint32_t height, ArrayRef<uint8_t> data) noexcept { ImTextureID add_texture(uint32_t width, uint32_t height, ArrayRef<uint8_t> data) noexcept {
const auto size = wgpu::Extent3D{ const auto size = wgpu::Extent3D{
.width = width, .width = width,
@ -26,7 +62,7 @@ ImTextureID add_texture(uint32_t width, uint32_t height, ArrayRef<uint8_t> data)
.mipLevelCount = 1, .mipLevelCount = 1,
.arrayLayerCount = 1, .arrayLayerCount = 1,
}; };
auto texture = g_Device.CreateTexture(&textureDescriptor); auto texture = g_device.CreateTexture(&textureDescriptor);
auto textureView = texture.CreateView(&textureViewDescriptor); auto textureView = texture.CreateView(&textureViewDescriptor);
{ {
const auto dstView = wgpu::ImageCopyTexture{ const auto dstView = wgpu::ImageCopyTexture{
@ -36,7 +72,7 @@ ImTextureID add_texture(uint32_t width, uint32_t height, ArrayRef<uint8_t> data)
.bytesPerRow = 4 * width, .bytesPerRow = 4 * width,
.rowsPerImage = height, .rowsPerImage = height,
}; };
g_Queue.WriteTexture(&dstView, data.data(), data.size(), &dataLayout, &size); g_queue.WriteTexture(&dstView, data.data(), data.size(), &dataLayout, &size);
} }
texture.Release(); // leak some memory! texture.Release(); // leak some memory!
return textureView.Release(); return textureView.Release();

18
aurora/lib/imgui.hpp Normal file
View File

@ -0,0 +1,18 @@
#pragma once
struct SDL_Window;
union SDL_Event;
namespace wgpu {
class RenderPassEncoder;
} // namespace wgpu
namespace aurora::imgui {
void create_context() noexcept;
void initialize(SDL_Window* window) noexcept;
void shutdown() noexcept;
void process_event(const SDL_Event& event) noexcept;
void new_frame() noexcept;
void render(const wgpu::RenderPassEncoder& pass) noexcept;
} // namespace aurora::imgui

2
extern/imgui vendored

@ -1 +1 @@
Subproject commit 5c8f8d031166765d2f1e2ac2de27df6d3691c05a Subproject commit dca527be1b77e38047054196fd6a96e8abdae131

View File

@ -23,203 +23,4 @@ struct Icon {
uint32_t height; uint32_t height;
}; };
Icon GetIcon(); Icon GetIcon();
enum class KeyCode {
/// The '1' key over the letters.
Key1,
/// The '2' key over the letters.
Key2,
/// The '3' key over the letters.
Key3,
/// The '4' key over the letters.
Key4,
/// The '5' key over the letters.
Key5,
/// The '6' key over the letters.
Key6,
/// The '7' key over the letters.
Key7,
/// The '8' key over the letters.
Key8,
/// The '9' key over the letters.
Key9,
/// The '0' key over the 'O' and 'P' keys.
Key0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
/// The Escape key, next to F1.
Escape,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
/// Print Screen/SysRq.
Snapshot,
/// Scroll Lock.
Scroll,
/// Pause/Break key, next to Scroll lock.
Pause,
/// `Insert`, next to Backspace.
Insert,
Home,
Delete,
End,
PageDown,
PageUp,
Left,
Up,
Right,
Down,
/// The Backspace key, right over Enter.
// TODO: rename
Back,
/// The Enter key.
Return,
/// The space bar.
Space,
/// The "Compose" key on Linux.
Compose,
Caret,
Numlock,
Numpad0,
Numpad1,
Numpad2,
Numpad3,
Numpad4,
Numpad5,
Numpad6,
Numpad7,
Numpad8,
Numpad9,
NumpadAdd,
NumpadDivide,
NumpadDecimal,
NumpadComma,
NumpadEnter,
NumpadEquals,
NumpadMultiply,
NumpadSubtract,
AbntC1,
AbntC2,
Apostrophe,
Apps,
Asterisk,
At,
Ax,
Backslash,
Calculator,
Capital,
Colon,
Comma,
Convert,
Equals,
Grave,
Kana,
Kanji,
LAlt,
LBracket,
LControl,
LShift,
LWin,
Mail,
MediaSelect,
MediaStop,
Minus,
Mute,
MyComputer,
// also called "Next"
NavigateForward,
// also called "Prior"
NavigateBackward,
NextTrack,
NoConvert,
OEM102,
Period,
PlayPause,
Plus,
Power,
PrevTrack,
RAlt,
RBracket,
RControl,
RShift,
RWin,
Semicolon,
Slash,
Sleep,
Stop,
Sysrq,
Tab,
Underline,
Unlabeled,
VolumeDown,
VolumeUp,
Wake,
WebBack,
WebFavorites,
WebForward,
WebHome,
WebRefresh,
WebSearch,
WebStop,
Yen,
Copy,
Paste,
Cut,
};
} // namespace metaforce } // namespace metaforce