428 lines
17 KiB
C++
428 lines
17 KiB
C++
//
|
|
// Created by Vicente Ferrari Smith on 06.03.26.
|
|
//
|
|
|
|
#include "webgpu.h"
|
|
#include <webgpu/webgpu.h>
|
|
#include <dawn/webgpu_cpp_print.h>
|
|
#include "renderer.h"
|
|
#include "../graphics.h"
|
|
#include <emscripten.h>
|
|
#include <emscripten/emscripten.h>
|
|
#include <iostream>
|
|
#include "utils_emscripten.h"
|
|
|
|
WGPUInstance instance;
|
|
WGPUAdapter adapter;
|
|
Device wgpu_device;
|
|
Queue wgpu_queue;
|
|
Surface wgpu_surface;
|
|
|
|
Renderer renderer;
|
|
|
|
void upload_texture(
|
|
const int w,
|
|
const int h,
|
|
const void *pixels,
|
|
Texture *texture)
|
|
{
|
|
|
|
}
|
|
|
|
std::string_view toStdStringView(WGPUStringView wgpuStringView) {
|
|
return
|
|
wgpuStringView.data == nullptr
|
|
? std::string_view()
|
|
: wgpuStringView.length == WGPU_STRLEN
|
|
? std::string_view(wgpuStringView.data)
|
|
: std::string_view(wgpuStringView.data, wgpuStringView.length);
|
|
}
|
|
|
|
WGPUStringView toWgpuStringView(std::string_view stdStringView) {
|
|
return { stdStringView.data(), stdStringView.size() };
|
|
}
|
|
WGPUStringView toWgpuStringView(const char* cString) {
|
|
return { cString, WGPU_STRLEN };
|
|
}
|
|
|
|
void sleepForMilliseconds(unsigned int milliseconds) {
|
|
#ifdef __EMSCRIPTEN__
|
|
emscripten_sleep(milliseconds);
|
|
#else
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Utility function to get a WebGPU adapter, so that
|
|
* WGPUAdapter adapter = requestAdapterSync(options);
|
|
* is roughly equivalent to the JavaScript
|
|
* const adapter = await navigator.gpu.requestAdapter(options);
|
|
*/
|
|
WGPUAdapter requestAdapterSync(WGPUInstance instance, WGPURequestAdapterOptions const * options) {
|
|
// A simple structure holding the local information shared with the
|
|
// onAdapterRequestEnded callback.
|
|
struct UserData {
|
|
WGPUAdapter adapter = nullptr;
|
|
bool requestEnded = false;
|
|
};
|
|
UserData userData;
|
|
|
|
// Callback called by wgpuInstanceRequestAdapter when the request returns
|
|
// This is a C++ lambda function, but could be any function defined in the
|
|
// global scope. It must be non-capturing (the brackets [] are empty) so
|
|
// that it behaves like a regular C function pointer, which is what
|
|
// wgpuInstanceRequestAdapter expects (WebGPU being a C API). The workaround
|
|
// is to convey what we want to capture through the userdata1 pointer,
|
|
// provided as the last argument of wgpuInstanceRequestAdapter and received
|
|
// by the callback as its last argument.
|
|
auto onAdapterRequestEnded = [](
|
|
WGPURequestAdapterStatus status,
|
|
WGPUAdapter adapter,
|
|
WGPUStringView message,
|
|
void* userdata1,
|
|
void* /* userdata2 */
|
|
) {
|
|
UserData& userData = *reinterpret_cast<UserData*>(userdata1);
|
|
if (status == WGPURequestAdapterStatus_Success) {
|
|
userData.adapter = adapter;
|
|
} else {
|
|
std::cerr << "Error while requesting adapter: " << toStdStringView(message) << std::endl;
|
|
}
|
|
userData.requestEnded = true;
|
|
};
|
|
|
|
// Build the callback info
|
|
WGPURequestAdapterCallbackInfo callbackInfo = {
|
|
/* nextInChain = */ nullptr,
|
|
/* mode = */ WGPUCallbackMode_AllowProcessEvents,
|
|
/* callback = */ onAdapterRequestEnded,
|
|
/* userdata1 = */ &userData,
|
|
/* userdata2 = */ nullptr
|
|
};
|
|
|
|
// Call to the WebGPU request adapter procedure
|
|
wgpuInstanceRequestAdapter(instance, options, callbackInfo);
|
|
|
|
// We wait until userData.requestEnded gets true
|
|
|
|
// Hand the execution to the WebGPU instance so that it can check for
|
|
// pending async operations, in which case it invokes our callbacks.
|
|
// NB: We test once before the loop not to wait for 200ms in case it is
|
|
// already ready
|
|
wgpuInstanceProcessEvents(instance);
|
|
|
|
while (!userData.requestEnded) {
|
|
// Waiting for 200 ms to avoid asking too often to process events
|
|
sleepForMilliseconds(200);
|
|
|
|
wgpuInstanceProcessEvents(instance);
|
|
}
|
|
|
|
return userData.adapter;
|
|
}
|
|
|
|
/**
|
|
* Utility function to get a WebGPU device, so that
|
|
* WGPUDevice device = requestDeviceSync(adapter, options);
|
|
* is roughly equivalent to
|
|
* const device = await adapter.requestDevice(descriptor);
|
|
* It is very similar to requestAdapter
|
|
*/
|
|
WGPUDevice requestDeviceSync(WGPUInstance instance, WGPUAdapter adapter, WGPUDeviceDescriptor const * descriptor) {
|
|
struct UserData {
|
|
WGPUDevice device = nullptr;
|
|
bool requestEnded = false;
|
|
};
|
|
UserData userData;
|
|
|
|
// The callback
|
|
auto onDeviceRequestEnded = [](
|
|
WGPURequestDeviceStatus status,
|
|
WGPUDevice device,
|
|
WGPUStringView message,
|
|
void* userdata1,
|
|
void* /* userdata2 */
|
|
) {
|
|
UserData& userData = *reinterpret_cast<UserData*>(userdata1);
|
|
if (status == WGPURequestDeviceStatus_Success) {
|
|
userData.device = device;
|
|
} else {
|
|
std::cerr << "Error while requesting device: " << toStdStringView(message) << std::endl;
|
|
}
|
|
userData.requestEnded = true;
|
|
};
|
|
|
|
// Build the callback info
|
|
WGPURequestDeviceCallbackInfo callbackInfo = {
|
|
/* nextInChain = */ nullptr,
|
|
/* mode = */ WGPUCallbackMode_AllowProcessEvents,
|
|
/* callback = */ onDeviceRequestEnded,
|
|
/* userdata1 = */ &userData,
|
|
/* userdata2 = */ nullptr
|
|
};
|
|
|
|
// Call to the WebGPU request adapter procedure
|
|
wgpuAdapterRequestDevice(adapter, descriptor, callbackInfo);
|
|
|
|
// Hand the execution to the WebGPU instance until the request ended
|
|
wgpuInstanceProcessEvents(instance);
|
|
while (!userData.requestEnded) {
|
|
sleepForMilliseconds(200);
|
|
wgpuInstanceProcessEvents(instance);
|
|
}
|
|
|
|
return userData.device;
|
|
}
|
|
|
|
void inspectAdapter(WGPUAdapter adapter) {
|
|
WGPULimits supportedLimits = {};
|
|
supportedLimits.nextInChain = nullptr;
|
|
|
|
bool success = wgpuAdapterGetLimits(adapter, &supportedLimits) == WGPUStatus_Success;
|
|
|
|
if (success) {
|
|
std::cout << "Adapter limits:" << std::endl;
|
|
std::cout << " - maxTextureDimension1D: " << supportedLimits.maxTextureDimension1D << std::endl;
|
|
std::cout << " - maxTextureDimension2D: " << supportedLimits.maxTextureDimension2D << std::endl;
|
|
std::cout << " - maxTextureDimension3D: " << supportedLimits.maxTextureDimension3D << std::endl;
|
|
std::cout << " - maxTextureArrayLayers: " << supportedLimits.maxTextureArrayLayers << std::endl;
|
|
}
|
|
|
|
// Prepare the struct where features will be listed
|
|
WGPUSupportedFeatures features;
|
|
|
|
// Get adapter features. This may allocate memory that we must later free with wgpuSupportedFeaturesFreeMembers()
|
|
wgpuAdapterGetFeatures(adapter, &features);
|
|
|
|
std::cout << "Adapter features:" << std::endl;
|
|
std::cout << std::hex; // Write integers as hexadecimal to ease comparison with webgpu.h literals
|
|
for (size_t i = 0; i < features.featureCount; ++i) {
|
|
std::cout << " - 0x" << features.features[i] << std::endl;
|
|
}
|
|
std::cout << std::dec; // Restore decimal numbers
|
|
|
|
// Free the memory that had potentially been allocated by wgpuAdapterGetFeatures()
|
|
wgpuSupportedFeaturesFreeMembers(features);
|
|
// One shall no longer use features beyond this line.
|
|
|
|
WGPUAdapterInfo properties;
|
|
properties.nextInChain = nullptr;
|
|
wgpuAdapterGetInfo(adapter, &properties);
|
|
std::cout << "Adapter properties:" << std::endl;
|
|
std::cout << " - vendorID: " << properties.vendorID << std::endl;
|
|
std::cout << " - vendorName: " << toStdStringView(properties.vendor) << std::endl;
|
|
std::cout << " - architecture: " << toStdStringView(properties.architecture) << std::endl;
|
|
std::cout << " - deviceID: " << properties.deviceID << std::endl;
|
|
std::cout << " - name: " << toStdStringView(properties.device) << std::endl;
|
|
std::cout << " - driverDescription: " << toStdStringView(properties.description) << std::endl;
|
|
std::cout << std::hex;
|
|
std::cout << " - adapterType: 0x" << properties.adapterType << std::endl;
|
|
std::cout << " - backendType: 0x" << properties.backendType << std::endl;
|
|
std::cout << std::dec; // Restore decimal numbers
|
|
wgpuAdapterInfoFreeMembers(properties);
|
|
}
|
|
|
|
// We create a utility function to inspect the device:
|
|
void inspectDevice(WGPUDevice device) {
|
|
|
|
WGPUSupportedFeatures features = WGPU_SUPPORTED_FEATURES_INIT;
|
|
wgpuDeviceGetFeatures(device, &features);
|
|
std::cout << "Device features:" << std::endl;
|
|
std::cout << std::hex;
|
|
for (size_t i = 0; i < features.featureCount; ++i) {
|
|
std::cout << " - 0x" << features.features[i] << std::endl;
|
|
}
|
|
std::cout << std::dec;
|
|
wgpuSupportedFeaturesFreeMembers(features);
|
|
|
|
WGPULimits limits = WGPU_LIMITS_INIT;
|
|
bool success = wgpuDeviceGetLimits(device, &limits) == WGPUStatus_Success;
|
|
|
|
if (success) {
|
|
std::cout << "Device limits:" << std::endl;
|
|
std::cout << " - maxTextureDimension1D: " << limits.maxTextureDimension1D << std::endl;
|
|
std::cout << " - maxTextureDimension2D: " << limits.maxTextureDimension2D << std::endl;
|
|
std::cout << " - maxTextureDimension3D: " << limits.maxTextureDimension3D << std::endl;
|
|
std::cout << " - maxTextureArrayLayers: " << limits.maxTextureArrayLayers << std::endl;
|
|
std::cout << " - maxBindGroups: " << limits.maxBindGroups << std::endl;
|
|
std::cout << " - maxBindGroupsPlusVertexBuffers: " << limits.maxBindGroupsPlusVertexBuffers << std::endl;
|
|
std::cout << " - maxBindingsPerBindGroup: " << limits.maxBindingsPerBindGroup << std::endl;
|
|
std::cout << " - maxDynamicUniformBuffersPerPipelineLayout: " << limits.maxDynamicUniformBuffersPerPipelineLayout << std::endl;
|
|
std::cout << " - maxDynamicStorageBuffersPerPipelineLayout: " << limits.maxDynamicStorageBuffersPerPipelineLayout << std::endl;
|
|
std::cout << " - maxSampledTexturesPerShaderStage: " << limits.maxSampledTexturesPerShaderStage << std::endl;
|
|
std::cout << " - maxSamplersPerShaderStage: " << limits.maxSamplersPerShaderStage << std::endl;
|
|
std::cout << " - maxStorageBuffersPerShaderStage: " << limits.maxStorageBuffersPerShaderStage << std::endl;
|
|
std::cout << " - maxStorageTexturesPerShaderStage: " << limits.maxStorageTexturesPerShaderStage << std::endl;
|
|
std::cout << " - maxUniformBuffersPerShaderStage: " << limits.maxUniformBuffersPerShaderStage << std::endl;
|
|
std::cout << " - maxUniformBufferBindingSize: " << limits.maxUniformBufferBindingSize << std::endl;
|
|
std::cout << " - maxStorageBufferBindingSize: " << limits.maxStorageBufferBindingSize << std::endl;
|
|
std::cout << " - minUniformBufferOffsetAlignment: " << limits.minUniformBufferOffsetAlignment << std::endl;
|
|
std::cout << " - minStorageBufferOffsetAlignment: " << limits.minStorageBufferOffsetAlignment << std::endl;
|
|
std::cout << " - maxVertexBuffers: " << limits.maxVertexBuffers << std::endl;
|
|
std::cout << " - maxBufferSize: " << limits.maxBufferSize << std::endl;
|
|
std::cout << " - maxVertexAttributes: " << limits.maxVertexAttributes << std::endl;
|
|
std::cout << " - maxVertexBufferArrayStride: " << limits.maxVertexBufferArrayStride << std::endl;
|
|
std::cout << " - maxInterStageShaderVariables: " << limits.maxInterStageShaderVariables << std::endl;
|
|
std::cout << " - maxColorAttachments: " << limits.maxColorAttachments << std::endl;
|
|
std::cout << " - maxColorAttachmentBytesPerSample: " << limits.maxColorAttachmentBytesPerSample << std::endl;
|
|
std::cout << " - maxComputeWorkgroupStorageSize: " << limits.maxComputeWorkgroupStorageSize << std::endl;
|
|
std::cout << " - maxComputeInvocationsPerWorkgroup: " << limits.maxComputeInvocationsPerWorkgroup << std::endl;
|
|
std::cout << " - maxComputeWorkgroupSizeX: " << limits.maxComputeWorkgroupSizeX << std::endl;
|
|
std::cout << " - maxComputeWorkgroupSizeY: " << limits.maxComputeWorkgroupSizeY << std::endl;
|
|
std::cout << " - maxComputeWorkgroupSizeZ: " << limits.maxComputeWorkgroupSizeZ << std::endl;
|
|
std::cout << " - maxComputeWorkgroupsPerDimension: " << limits.maxComputeWorkgroupsPerDimension << std::endl;
|
|
// std::cout << " - maxStorageBuffersInVertexStage: " << limits.maxStorageBuffersInVertexStage << std::endl;
|
|
// std::cout << " - maxStorageTexturesInVertexStage: " << limits.maxStorageTexturesInVertexStage << std::endl;
|
|
// std::cout << " - maxStorageBuffersInFragmentStage: " << limits.maxStorageBuffersInFragmentStage << std::endl;
|
|
// std::cout << " - maxStorageTexturesInFragmentStage: " << limits.maxStorageTexturesInFragmentStage << std::endl;
|
|
}
|
|
}
|
|
|
|
void create_surface(GLFWwindow *window) {
|
|
wgpu_surface.surface = wgpuGlfwCreateSurfaceForWindow(instance, window);
|
|
|
|
WGPUSurfaceConfiguration config = WGPU_SURFACE_CONFIGURATION_INIT;
|
|
config.width = 640;
|
|
config.height = 480;
|
|
config.device = wgpu_device.device;
|
|
|
|
// We initialize an empty capability struct:
|
|
WGPUSurfaceCapabilities capabilities = WGPU_SURFACE_CAPABILITIES_INIT;
|
|
|
|
// We get the capabilities for a pair of (surface, adapter).
|
|
// If it works, this populates the `capabilities` structure
|
|
WGPUStatus status = wgpuSurfaceGetCapabilities(wgpu_surface.surface, adapter, &capabilities);
|
|
if (status != WGPUStatus_Success) {
|
|
return;
|
|
}
|
|
|
|
// From the capabilities, we get the preferred format: it is always the first one!
|
|
// (NB: There is always at least 1 format if the GetCapabilities was successful)
|
|
config.format = capabilities.formats[0];
|
|
|
|
// We no longer need to access the capabilities, so we release their memory.
|
|
wgpuSurfaceCapabilitiesFreeMembers(capabilities);
|
|
|
|
wgpuSurfaceConfigure(wgpu_surface.surface, &config);
|
|
}
|
|
|
|
WGPUTextureView get_next_surface_view() {
|
|
WGPUSurfaceTexture surfaceTexture = WGPU_SURFACE_TEXTURE_INIT;
|
|
wgpuSurfaceGetCurrentTexture(wgpu_surface.surface, &surfaceTexture);
|
|
|
|
WGPUTextureViewDescriptor viewDescriptor = WGPU_TEXTURE_VIEW_DESCRIPTOR_INIT;
|
|
viewDescriptor.label = toWgpuStringView("Surface texture view");
|
|
viewDescriptor.dimension = WGPUTextureViewDimension_2D; // not to confuse with 2DArray
|
|
WGPUTextureView target_view = wgpuTextureCreateView(surfaceTexture.texture, &viewDescriptor);
|
|
|
|
// We no longer need the texture, only its view,
|
|
// so we release it at the end of GetNextSurfaceViewData
|
|
wgpuTextureRelease(surfaceTexture.texture);
|
|
return target_view;
|
|
}
|
|
|
|
void create_device() {
|
|
std::cout << "Requesting adapter..." << std::endl;
|
|
|
|
WGPURequestAdapterOptions adapterOpts = {};
|
|
adapterOpts.nextInChain = nullptr;
|
|
adapter = requestAdapterSync(instance, &adapterOpts);
|
|
|
|
std::cout << "Got adapter: " << adapter << std::endl;
|
|
|
|
inspectAdapter(adapter);
|
|
|
|
std::cout << "Requesting device..." << std::endl;
|
|
|
|
WGPUDeviceDescriptor deviceDesc = WGPU_DEVICE_DESCRIPTOR_INIT;
|
|
// Any name works here, that's your call
|
|
deviceDesc.label = toWgpuStringView("My Device");
|
|
std::vector<WGPUFeatureName> features;
|
|
// No required feature for now
|
|
deviceDesc.requiredFeatureCount = features.size();
|
|
deviceDesc.requiredFeatures = features.data();
|
|
// Make sure 'features' lives until the call to wgpuAdapterRequestDevice!
|
|
WGPULimits requiredLimits = WGPU_LIMITS_INIT;
|
|
// We leave 'requiredLimits' untouched for now
|
|
deviceDesc.requiredLimits = &requiredLimits;
|
|
// Make sure that the 'requiredLimits' variable lives until the call to wgpuAdapterRequestDevice!
|
|
deviceDesc.defaultQueue.label = toWgpuStringView("The Default Queue");
|
|
auto onDeviceLost = [](
|
|
WGPUDevice const * device,
|
|
WGPUDeviceLostReason reason,
|
|
struct WGPUStringView message,
|
|
void* /* userdata1 */,
|
|
void* /* userdata2 */
|
|
) {
|
|
// All we do is display a message when the device is lost
|
|
std::cout
|
|
<< "Device " << device << " was lost: reason " << reason
|
|
<< " (" << toStdStringView(message) << ")"
|
|
<< std::endl;
|
|
};
|
|
deviceDesc.deviceLostCallbackInfo.callback = onDeviceLost;
|
|
deviceDesc.deviceLostCallbackInfo.mode = WGPUCallbackMode_AllowProcessEvents;
|
|
auto onDeviceError = [](
|
|
WGPUDevice const * device,
|
|
WGPUErrorType type,
|
|
struct WGPUStringView message,
|
|
void* /* userdata1 */,
|
|
void* /* userdata2 */
|
|
) {
|
|
std::cout
|
|
<< "Uncaptured error in device " << device << ": type " << type
|
|
<< " (" << toStdStringView(message) << ")"
|
|
<< std::endl;
|
|
};
|
|
deviceDesc.uncapturedErrorCallbackInfo.callback = onDeviceError;
|
|
wgpu_device.device = requestDeviceSync(instance, adapter, &deviceDesc);
|
|
|
|
std::cout << "Got device: " << wgpu_device.device << std::endl;
|
|
// We no longer need to access the adapter once we have the device
|
|
wgpuAdapterRelease(adapter);
|
|
inspectDevice(wgpu_device.device);
|
|
|
|
wgpu_queue.queue = wgpuDeviceGetQueue(wgpu_device.device);
|
|
}
|
|
|
|
void create_instance() {
|
|
// We create a descriptor
|
|
WGPUInstanceDescriptor desc = WGPU_INSTANCE_DESCRIPTOR_INIT;
|
|
desc.nextInChain = nullptr;
|
|
|
|
// We create the instance using this descriptor
|
|
#ifdef __EMSCRIPTEN__
|
|
instance = wgpuCreateInstance(nullptr);
|
|
#else
|
|
instance = wgpuCreateInstance(&desc);
|
|
#endif
|
|
}
|
|
|
|
void platform_graphics_init(GLFWwindow *window) {
|
|
create_instance();
|
|
|
|
create_device();
|
|
|
|
create_surface(window);
|
|
|
|
renderer = Renderer(window);
|
|
}
|
|
|
|
void graphics_deinit() {
|
|
|
|
}
|
|
|
|
void begin_frame() {
|
|
renderer.begin_frame();
|
|
}
|
|
|
|
void end_frame(GLFWwindow *window) {
|
|
|
|
renderer.end_frame(window);
|
|
}
|
|
|
|
void submit_sprite(glm::vec2 pos, const sprite_t &sprite) {
|
|
renderer.submit_sprite({pos.x, pos.y}, sprite);
|
|
}
|