GPU: Experimental WebGPU SDLGPU Backend - #16020
Conversation
Currently only supports Wayland, and it's quite..... bad This is a DRAFT! Just a small proof-of-concept. I'll submit a draft PR to the SDL project to see if I should continue development. If this is greenlit, I might making consider a WGPU SDL_GPU backend if wgpu-native is in a good enough state (maybe Dawn if it isn't?) No promises though! Don't get angry at me if I don't! Current known issues: 1: Error handling is sparse. 2: It only supports the shared library version of wgpu-native, and it does that by just loading it whenever it needs to do something 3: wgpu-native is bad and doesn't implement wgpuGetProcAddress so we're just using SDL_LoadFunction instead. I'd much prefer using the native wgpuGetProcAddress but alas....
Removed webgpu.h and instead we're defining the types ourselves Thank you @HamdyElzanqali for pointing out my stupidity lol
wgpu_native really seems to like statically linking itself (as shown by it not implementing the functionality for dynamic linking), so I added an option to statically link it alongside SDL. How I implemented this almost certainly goes against some convention or guideline in SDL but I'm shooting blind here. I have no idea what to do.
Added Windows support for WGPU, although it is untested since I don't have a Windows machine to test it on.
Added support for Google's Dawn WebGPU implementation. You control which one's used via the option "SDL_WGPU_LIB" where it's either "wgpu-native" or "dawn".
Congratulations; you can now use WebGPU on the Web. I'll make an example eventually, but it's midnight in Sweden and I gotta go to bed.
I'm calling it that simply because it's quicker.
Oh my god, there's SO MUCH STUFF TO DO
Keep on buffering Denji I'm losing my mind
I am so confused here. This backend is effectively a port of the Vulkan backend, but just with WebGPU instead, but that means that I'm often including redundant functionality or attempt to create things that just don't work in WebGPU This backend will have to be polished, as right now it's about as smooth as 1 grit sandpaper.
Since WebGPU shader modules can have multiple types of stages (one shader can have a vertex, fragment, and compute stage in it), I had to make a workaround. I'm actually sorta proud of my solution! How it works is that every "SDL_GPUShader" is actually a "WebGPUShaderReference", which stores the entrypoint, what stage it is, and a hash of the shader source code. That hash is used as the key into a hash table in the renderer, which contains the "actual" shader. I'm bad at explaining things, so I'd just read the code. It's only two functions and I hope I made it easy to read.
YOU WILL GET A 200 LINE FUNCTION AND YOU WILL MERGE IT
Or, I mean; I actually started work on this three days ago. I just hate WebGPU bind groups so much that I've actively procrastinated on implementing them. I will be the first to say this: My implementation of bindings in WebGPU SUCKS. I will not deny that. I made this quickly, and badly, since if this is not done; I will be permanently stuck in a state of procrastination, since I cannot work on multiple things at once. Something something neurodivergence, something something ADHD. Sorry for the lack of commits in the past few days.
Uniforms'll require some extra work since WebGPU expects to receive a buffer while SDL_GPU only provides raw data on the CPU side
I've got it compiling, now I just gotta fix why it crashes.
Why's nothing showing up on the screen 😭
That is pretty bad. I'll push an update to patch it at the shader level until a better solution is found. But if it's really a fundamental limitation of the backend, we have to document it. |
I don't think there should be anything that's calling any SDL-GPU functions from non-main threads. Taisei offloads operations like actual texture uploads and shader compiles etc. to the main thread, because the thing was designed with OpenGL in mind where multithreading is also a giant pain. It's interesting that it causes problems here. I've been running with threading disabled for now out of caution though, I'll try flipping it on a bit later and see what breaks… |
|
this backend's fucking cursed |
|
Pushed a fix for uniformity errors in Taisei. It's actually not nearly as bad as I thought: you can completely disable those errors by adding Note: after you pull, you may need to do With the fix applied, the game loads a bit further, until this happens. You should probably enable all those texture format features by default to match the other backends, at least opportunistically. |
Tier1 isn't enabled because it's only supported by 2% of browsers. I'll mark Tier1 formats as unsupported. (why didn't i do that earlier?) For the uniformity diagnostic, thankyouthankyouthankyouthankyouthankyouthankyouthankyouthankyou thankyouthankyouthankyouthankyou!!! Also: I fixed the stride issue. My mind instantly just went to invalid padding since that's been an issue more times than I can count 😅 The font atlas is being uploaded wrong, I noticed that yesterday but chose to ignore it since there were larger issues, so I'll look at that next. |
I think this site might not be up to date with the spec. First of all, it lists What does exist, is a |
|
https://webgpu.report/ might be more reliable (though there's not a lot of data). It acknowledges |
Yeah, after checking it on my devices, my iPhone 17, my MacBook, my Linux laptop and my Linux PC all support tier1, so I'll add it to the optional features. |
|
As if there aren't enough complications with WebGPU, here's one more thing I've been doing as part of reviewing this... At least on the FNA side the conclusion we came to is that the shader compiler ecosystem for WGSL is somehow simultaneously over-engineered and half-baked; there are only so many ways to generate WGSL from standard shader formats and they're all huge standalone projects that would be really difficult to integrate into SDL_shadercross, so I ended up pulling from my SPIR-V support for SDL-playstation to build this: You start with a folder of SPIR-V shaders (in our case it's generated because D3D9...), and with Tint you can run this: #!/bin/bash
set -e
if [ -z "$1" ]; then
TRACE="FNA3D_Trace.bin"
SPIRV="FNA3D_Trace.bin.spirv"
WGSL="FNA3D_Trace.bin.wgsl"
else
TRACE="$1"
SPIRV="$1.spirv"
WGSL="$1.wgsl"
fi
cd "`dirname "$0"`"
if [ ! -d $SPIRV ]; then
echo "Dumping SPIR-V..."
./fna3d_dumpspirv $TRACE
fi
if [ ! -d $SPIRV ]; then
exit 1
fi
echo "Generating WGSL..."
mkdir -p $WGSL
FILES=`ls "$SPIRV" | grep '\.vert.spv$'`
for f in $FILES
do
./Dawn/bin/tint "$SPIRV/$f" -o "$WGSL/`basename $f .spv`.wgsl"
done
FILES=`ls "$SPIRV" | grep '\.frag.spv$'`
for f in $FILES
do
./Dawn/bin/tint --allow-non-uniform-derivatives "$SPIRV/$f" -o "$WGSL/`basename $f .spv`.wgsl"
done
echo "Building shaders.wgsl.bin..."
dotnet buildCache.cs $@using System.IO;
using (FileStream fileOut = File.OpenWrite("shaders.wgsl.bin"))
using (BinaryWriter fileWriter = new BinaryWriter(fileOut))
{
string[] wgsl = Directory.GetFiles((args.Length > 0) ? (args[0] + ".wgsl") : "FNA3D_Trace.bin.wgsl");
fileWriter.Write(wgsl.Length);
foreach (string file in wgsl)
{
uint crc32 = Convert.ToUInt32(Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(file)), 16);
byte[] shader = File.ReadAllBytes(file);
fileWriter.Write(crc32);
fileWriter.Write(shader.Length);
fileWriter.Write(shader);
}
}This builds everything to WGSL with one of the obnoxious errors suppressed, and then a quick C# program generates a binary blob (you could probably convert this back to bash, I'm just bad with xxd and friends) which can then be used by the SDL_GPU backend to support SPIR-V via a phony on-disk shader cache: // Somewhere in WGPU_CreateDevice...
extern SDL_HashTable *spirvCache;
SDL_IOStream *spirv = SDL_IOFromFile("shaders.wgsl.bin", "rb");
if (spirv != NULL) {
Uint32 numEntries;
SDL_ReadIO(spirv, &numEntries, sizeof(Uint32));
spirvCache = SDL_CreateHashTable(numEntries, 0, SDL_HashID, SDL_KeyMatchID, SDL_DestroyHashValue, NULL);
for (Uint32 i = 0; i < numEntries; i += 1) {
Uint32 crc;
SDL_ReadIO(spirv, &crc, sizeof(Uint32));
Uint32 shaderLen;
SDL_ReadIO(spirv, &shaderLen, sizeof(Uint32));
void *shader = SDL_malloc(shaderLen);
SDL_ReadIO(spirv, shader, shaderLen);
SDL_InsertIntoHashTable(spirvCache, (void*) (size_t) crc, shader, false);
}
SDL_CloseIO(spirv);
}
extern SDL_GPUDevice* result;
if (spirvCache != NULL) {
result->shader_formats |= SDL_GPU_SHADERFORMAT_SPIRV;
}// Somewhere in WGPU_CreateGPUShader
extern SDL_HashTable *spirvCache;
const void *code;
if (createinfo->format == SDL_GPU_SHADERFORMAT_SPIRV)
{
Uint32 crc = SDL_crc32(0, createinfo->code, createinfo->code_size);
if (!SDL_FindInHashTable(spirvCache, (void*) (size_t) crc, &code)) {
char* path;
SDL_asprintf(&path, "%sbroken_%x.%s.spv", SDL_GetPrefPath("libsdl-org", "SDL_gpu"), crc, createinfo->stage == SDL_GPU_SHADERSTAGE_FRAGMENT ? "frag" : "vert");
if (path != NULL) {
SDL_IOStream* io = SDL_IOFromFile(path, "wb");
if (io != NULL) {
SDL_WriteIO(io, createinfo->code, createinfo->code_size);
SDL_CloseIO(io);
SDL_Log("Missing SPIR-V dumped to %s", path);
}
SDL_free(path);
}
SDL_assert(!"SPIR-V was not found in the WGSL cache!");
}
}
else
{
SDL_assert(createinfo->format == SDL_GPU_SHADERFORMAT_WGSL);
code = createinfo->code;
}Like I said before, this idea is already shipping in PlayStation titles, but the difference here is that technically you can build WGSL at runtime, it's just horrifically inconvenient to do so, to the point where everything you just read was far easier to achieve. With all that in mind, does anybody object to including this in the WebGPU renderer? I can do the integration myself if so, and once that's in place we should be able to start running FNA3D traces to test the whole thing properly. |
That looks nasty. Why not do it on the FNA3D side? Also, I definitely would not trust that crc32 hash to never collide, especially in a large project.
I think naga + spirv-webgpu-transform can work for SDL_shadercross, despite the Rust. With just spirv->wgsl support, it compiles down to about 1 megabyte. The most obvious downside is that you have to write a little Rust glue code (since naga doesn't have official C bindings), and integrate Cargo into your build system (that's the yuckiest part). I managed to make it work for runtime translation in Taisei, though I definitely don't love the Cargo "integration". If you're interested, here's how I did it.
Tint seems like it should be easier to integrate, with it being written in C++, but it's a google project, and google's apparent policy is to make their crap as painful as possible to integrate into non-google projects, so you need depot_tools and gclient and all that garbage to build it… so I didn't bother. Maybe I'll write a minimal meson build system for it one day, but probably not. |
|
I don't really get it. Are we doing this so that we could use the WebGPU backend on FNA applications that don't have support for it? UPDATE: Alright, after looking further into MojoShader and FNA I've only become more confused. But either way, I don't really understand how this would be the backend's responsibility? This is a WebGPU backend. WebGPU only supports WGSL. Why would we add support for SPIR-V? Especially so considering that this caching solution requires converting the shaders to WGSL anyways? Why not just use those? |
|
Is debug mode on? In my infinite wisdom, I made the backend only print WebGPU errors if debug mode is on. |
|
Somehow failed to notice that FNA3D Replay wasn't enabling debug mode since the trace wasn't marked as debug, doh Now we get something: I think we had to deal with this for either Vulkan or D3D, don't remember if they just allowed this by default |
|
Index 0 in |
|
With the sampler state issue fixed, we get something more expected: BTW if you want our traces to test against, email me (username at username dot com) and I'll also send over quick build instructions for the replayer with WGSL enabled. |
[sdl-ci-filter msys2-mingw64] [sdl-ci-filter ubuntu-latest]
|
It's trying to bind a texture to a shader that has no texture bindings? Do we have the WGSL shader source on hand? |
|
Maybe it's Fragment Vertex |
|
|
||
| #define WEBGPU_INTERNAL_RequiredFeaturesCount 4 | ||
| #define WEBGPU_INTERNAL_OptionalFeaturesCount 7 | ||
| #define WEBGPU_INTERNAL_OptionalFeaturesCount 9 |
There was a problem hiding this comment.
You can omit explicit sizes and just use SDL_arraysize(whatever) in loops etc.
| if (createInfo->vertex_input_state.vertex_attributes[j].buffer_slot == i) { | ||
| const SDL_GPUVertexAttribute *attr = &createInfo->vertex_input_state.vertex_attributes[j]; | ||
| if (attr->buffer_slot == createInfo->vertex_input_state.vertex_buffer_descriptions[i].slot) { | ||
| arrayStride = attr->offset + SizeOfSDLVertexFormat[attr->format]; |
There was a problem hiding this comment.
I don't understand this… What is going on here? Why not just take the SDL_GPUVertexBufferDescription::pitch value? Seems like it's completely ignored. I think if whatever this is works for Taisei, it's probably an accident…
There was a problem hiding this comment.
I think I made it do this a couple months ago for some reason, and since then I've forgotten the pitch value exists.
I gotta check my pipes for lead one of these days I swear to god
There was a problem hiding this comment.
I guess you are just assuming that all attributes are interleaved and densely packed with no spacing in-between. But this will fail if the attributes are planar or have extra spacing. Just use the pitch value, it is the stride.
Can't be, you don't even have any branching in that shader. Does it not compile without suppressing the diagnostic? |
|
@flibitijibibo just a guess, is it possible that the original vertex shader had an unused sampler that got pruned somewhere down the translation pipeline, but the rendering code assumes it's still there and tries to bind it? Check the backtrace, should tell you if the faulty bind group belongs to the vertex or fragment stage. |
"What idiot coded this!!??!" - Idiot who coded this.
| case SDL_GPU_TEXTUREFORMAT_R16G16_UNORM: | ||
| case SDL_GPU_TEXTUREFORMAT_R16G16B16A16_UNORM: | ||
| case SDL_GPU_TEXTUREFORMAT_R10G10B10A2_UNORM: | ||
| return !hasDepthUsage && !hasReadWriteStorageUsage && !hasSamplerUsage && wgpuDeviceHasFeature(renderer->device, WGPUFeatureName_TextureFormatsTier1); |
There was a problem hiding this comment.
I was running the build in my local copy, and hasSamplerUsage doesn't seem to be declared. It's currently preventing the build from completing on my end.
There was a problem hiding this comment.
ooops should be fixed now
|
Thanks for all the work you have done on the WebGPU backend. Wanted to share something I found while testing it with my own renderer. One of the shaders I have uses a storage buffer struct, so when running it, I would come across: tint generates that part of the shader as: struct LightingStorage {
lightPositions : array<vec4<f32>, 32u>,
lightColors : array<vec4<f32>, 32u>,
lightFlicker : array<vec4<f32>, 32u>,
walls : array<vec4<f32>, 32u>,
}
@group(2u) @binding(2u) var<storage, read> lightingStorage : LightingStorage;
...Manually changing the binding to an array got the shader running: struct LightingStorage {
lightPositions : array<vec4<f32>, 32u>,
lightColors : array<vec4<f32>, 32u>,
lightFlicker : array<vec4<f32>, 32u>,
walls : array<vec4<f32>, 32u>,
}
@group(2u) @binding(2u) var<storage, read> lightingStorage : array<LightingStorage>;
...
}I wanted to see if it was possible to get the shader to work without manually overriding the tint-generated shader (or creating tooling that does). What I tried was changing the whitelist in I don't think it's a proper solution either, since there might be other things that may need to be accounted for. I also noticed the tint-generated shaders for ComputeSpriteBatch_Example and PullSpriteBatch_Example use a storage buffer binding with a struct type. |





Description
This is my SDLGPU WebGPU backend. I've been working on it for about 1 and a half months, and it's reached a level of "finishedness" where I need feedback on how it works.
As of right now it can run 34 out of the 34 SDLGPU examples, and I've got it running on Windows, Linux, and the Web.
It currently supports most of the SDLGPU standard, however I'm making it a draft PR for a few reasons.
This is the first time I've ever contributed to any project that's not my own (my hubris knows no bounds), so please don't murder me when I do something stupid.
I'll be happy to help with any issues that'll inevitably be found.
(Note that I'm Swedish, so if you're in the Americas I'll be at least ~6 hours behind you.)
(Also, read the README-WEBGPU.md file. Please.)
(TLDR: WebGPU has tortured me and I will further inflict this pain upon others by forcing them to review my code.)
Before this can actually be considered for merging into SDL, we'll need to do a lot of things.
SDL_WEBGPU_USE_PORTSCMake option, by default on)SDL_DownloadFromGPUBuffer... My genius frightens me.//!SDLGPU_COMPAT_F32_UNFILTERABLEsomewhere within your shader code to use non-filterable texture types. This is bad, as not only does it force unfilterable sampling for ALL samplers regardless of type, it is also confusing, as this is also needed for sampling depth textures.Anyways, I'm gonna go play CloverPit now. Have fun!
(Or don't; I'm not your dad.)
Existing Issue(s)
Resolves issue 10768.