Skip to content

GPU: Experimental WebGPU SDLGPU Backend - #16020

Open
TheeStickmahn wants to merge 110 commits into
libsdl-org:mainfrom
TheeStickmahn:main
Open

GPU: Experimental WebGPU SDLGPU Backend#16020
TheeStickmahn wants to merge 110 commits into
libsdl-org:mainfrom
TheeStickmahn:main

Conversation

@TheeStickmahn

@TheeStickmahn TheeStickmahn commented Jul 18, 2026

Copy link
Copy Markdown
  • I confirm that I am the author of this code and release it to the SDL project under the Zlib license. This contribution does not contain code from other sources, including code generated by a Large Language Model ("AI").

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.

  • 1: It's not finished.
  • 2: Some parts of the backend are shall we say..... oddly designed.
  • 3: I've seen ancient Egyptian hieroglyphics more legible than this code.
  • 4: This codebase is filled with some of the most deranged & demented code I've ever written.
    • 4.5: WebGPU drove me insane, and some of that insanity spilled over into the code comments. They're not very professional.

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.

  • Remake CMake shenanigans.
    • The CMake has now been remade in order to make the build process easier and more intuitive. If compiling natively, it'll search for a static Dawn library in the CMake source directory. On Emscripten, it'll automatically link Emdawnwebgpu through Emscripten's official remote port.
      • (This behaviour can be toggled through the SDL_WEBGPU_USE_PORTS CMake option, by default on)
  • Make sure it works on all platforms.
    • Linux
    • Web. (see the web demo: https://gpuexamples.thestickmahn.dev)
      • Chrome & Chromium derived.
      • Firefox*
        • Works, but has a bug when it comes to switching examples. Firefox dislikes it when you destroy a window before it's been presented (not 100% sure but let's pretend I know what I'm doing), which means that if you switch examples too fast, it'll freeze until you switch to another tab and then go back to the examples. NOTE: This bug is seemingly fixed in Firefox Beta 154.
      • Safari
    • Windows
    • MacOS (Haven't added video support yet. Objective-C is a horrifying language.)
    • iOS (Also no video support)
    • Android (Don't have an Android phone.) (Also, no video support)
  • Implement unimplemented features
    • Buffer cycling.
    • Texture cycling.
    • Mipmap generation.
    • Pretty much none of the backend's code is thread safe. We should make it thread safe.
    • Numerous other things I've certainly forgotten.
  • Add runtime error handling & validation. The backend currently has very sparse error handling, and allows you to shoot yourself in the foot with ease.
  • WebGPU features. (Sorta half-done, there's no way to configure the feature set.)
  • Bug fixes.
    • Downloading from buffers didn't work, because I hadn't implemented SDL_DownloadFromGPUBuffer... My genius frightens me.
    • Fix all memory leaks.
    • The shader parser doesn't support all valid WGSL.
      • This is largely due to the parser's crude nature. It doesn't understand some key words, and features such as binding a resource to a struct are unsupported entirely.
    • Stencils were broken. They're fixed now :)
    • Issues with bindings and bind groups.
      • Textures with non-filterable texture types are implemented horribly. You have to put //!SDLGPU_COMPAT_F32_UNFILTERABLE somewhere 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.
        • UPDATE (2026-07-25): The WebGPU feature which allows for filterable float32 textures is now automatically requested. It's optional, since there's only about 90% support for the feature, but on most devices it should just work OOTB. We should still figure out a solution for this though, as there's still the issue of sampling depth textures.
    • Remove support for wgpu-native and switch fully to Dawn.
      • As of Aug. 11th, 2026, wgpu-native is no longer supported in the WebGPU backend. It is suggested you switch to Dawn instead.
  • Remove redundant code.
  • Numerous other things I've certainly forgotten.
  • Not necessarily something that's a must, but we should add SPIRV -> WGSL support to SDL_shadercross.

Anyways, I'm gonna go play CloverPit now. Have fun!
(Or don't; I'm not your dad.)

Existing Issue(s)

Resolves issue 10768.

image

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 😭
@Akaricchi

Akaricchi commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

The weird text comes from the fact that the verticies are not padded correctly in the shader, since GLSL and WGSL have different rules regarding alignment.

I don't think that's quite it. I'm poking at a renderdoc frame capture right now. I don't get why the background isn't drawn correctly yet, but noticed something weird about the draw call that's supposed to draw the logo.

The input attributes at the vertex stage are quite wrong, resulting in this weird shape:
Screenshot from 2026-08-27 15-16-51

But this is actually caused by incorrect stride set for the 0th buffer. Notice how both buffers have it set to 184. That is correct for buffer 1 (sprite instancing data), but not for buffer 0 (mesh geometry data). It should be 44.
Screenshot from 2026-08-27 15-17-05

This is not a value set by WGSL, but part of the pipeline state. I think the value for buffer 1 somehow ended up overriding the one for buffer 0.

@Akaricchi

Copy link
Copy Markdown
Contributor

The "non-uniform" errors are not my choice. It's a fundamental limitation of WGSL, and one that I have found no solution around.

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.

@Akaricchi

Copy link
Copy Markdown
Contributor

Also, I had to fully turn off asynchronous loading. Multithreaded Dawn is insanely unstable. This is an issue I am actively aware of and working on, but there is literally NO documentation regarding it that I could find, so it's exceedingly difficult to understand why it breaks. (Especially considering that breaks differently on different graphics cards)

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…

@TheeStickmahn

Copy link
Copy Markdown
Author

this backend's fucking cursed

@Akaricchi

Copy link
Copy Markdown
Contributor

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 diagnostic(off, derivative_uniformity); on top of the shader. So I just made the translator spit that out.

Note: after you pull, you may need to do rm -rf ~/.cache/taisei/shaders to see the changes. Taisei caches translated shaders for faster future load times. Usually I'd bump the cache version for a change like this, but I haven't done that here since the whole thing hasn't landed in master yet.

With the fix applied, the game loads a bit further, until this happens.

Unsupported texture format TextureFormat::RGBA16Unorm, reason: requires feature FeatureName::TextureFormatsTier1.
 - While validating [TextureDescriptor].
 - While calling [Device].CreateTexture([TextureDescriptor]).

You should probably enable all those texture format features by default to match the other backends, at least opportunistically.

@TheeStickmahn

Copy link
Copy Markdown
Author

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.
image

@Akaricchi

Akaricchi commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

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?)

I think this site might not be up to date with the spec. First of all, it lists texture_formats_tier1 as a "WGSL language feature", which is weird. That name does not exist within documented WGSL extensions, and it frankly does not make sense for this to have anything to do with the shading language. And indeed, the site reports it as unsupported by my browser.

What does exist, is a GPUFeatureName called texture-formats-tier1, but it's missing from that site. If I check here and here, my browser in fact does support both tier1 and tier2. I suspect that tier1 has broad support across modern hardware, but I don't know if there's a database to verify that.

@Akaricchi

Akaricchi commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

https://webgpu.report/ might be more reliable (though there's not a lot of data). It acknowledges texture-formats-tier1 and texture-formats-tier2 as WebGPU features, and claims 64% global support for them. Which is still not great, but probably enough to enable both for parity with the other backends, especially since there're a query API.

@TheeStickmahn

Copy link
Copy Markdown
Author

https://webgpu.report/ might be more reliable (though there's not a lot of data). It acknowledges texture-formats-tier1 and texture-formats-tier2 as WebGPU features, and claims 64% global support for them. Which is still not great, but probably enough to enable both for parity with the other backends, especially since there're a query API.

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.

@flibitijibibo

flibitijibibo commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

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.

@Akaricchi

Akaricchi commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

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:

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.

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

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.

@TheeStickmahn

TheeStickmahn commented Aug 28, 2026

Copy link
Copy Markdown
Author

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?

@flibitijibibo

flibitijibibo commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Ended up moving this to MojoShader, which is the one that calls CreateGPUShader in our case:

icculus/mojoshader#86

With that I can do tests with the PR as-is, the result was... not much yet!

image

It's not crashing (except for Cancel() because it's not started yet), but I'm also not seeing any errors either. The Wizorb test is just a single basic 2D sprite shader.

@TheeStickmahn

TheeStickmahn commented Aug 28, 2026

Copy link
Copy Markdown
Author

Is debug mode on? In my infinite wisdom, I made the backend only print WebGPU errors if debug mode is on.

@flibitijibibo

Copy link
Copy Markdown
Collaborator

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:

flibitijibibo@flibitDemon:~/Programming/csLibraries/FNA3D/flibitBuild3$ SDL3_DYNAMIC_API=../../../cppLibraries/SDL_wgpu/flibitBuild/libSDL3.so.0 SDL_GPU_DRIVER=webgpu ./fna3d_replay -debug FNA3D_Trace_Wizorb.bin
Pixel density is 1.500000
FNA3D tracing disabled!
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: This backend is EXPERIMENTAL. Don't be surprised if it breaks, be surprised if it doesn't.
An Emscripten web demo is available at https://thestickmahn.gitlab.io/ihatenamingthings/
Please report any issues to the Github repo.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Acquired WebGPU adapter!
WebGPU adapter does not support optional feature "WGPUFeatureName_TextureCompressionASTC".
WebGPU adapter does not support optional feature "WGPUFeatureName_TextureCompressionASTCSliced3D".
Acquired WebGPU device!
ERROR: WebGPU uncaptured error!
One of minFilter (FilterMode::Nearest), magFilter (FilterMode::Nearest) or mipmapFilter (MipmapFilterMode::Nearest) is not FilterMode::Linear while using anisotropic filter (maxAnisotropy is 4.000000)
 - While validating [SamplerDescriptor]
 - While calling [Device].CreateSampler([SamplerDescriptor]).
WARNING: 

Assertion failure at WEBGPU_INTERNAL_UncapturedErrorCallback (SDL_gpu_webgpu.c:1508), triggered 1 time:
  '!"Uncaptured WebGPU error! SDL won't let me format this message though so check the console or smth"'


(zenity:98408): Adwaita-WARNING **: 15:32:03.015: Using GtkSettings:gtk-application-prefer-dark-theme with libadwaita is unsupported. Please use AdwStyleManager:color-scheme instead.
Aborted                    (core dumped) SDL3_DYNAMIC_API=../../../cppLibraries/SDL_wgpu/flibitBuild/libSDL3.so.0 SDL_GPU_DRIVER=webgpu ./fna3d_replay -debug FNA3D_Trace_Wizorb.bin

I think we had to deal with this for either Vulkan or D3D, don't remember if they just allowed this by default

@TheeStickmahn

Copy link
Copy Markdown
Author

Index 0 in SDLToWebGPU_(Mipmap)FilterMode is "Nearest", so it's possible that it's just being left uninitialized?

Comment thread src/gpu/webgpu/SDL_gpu_webgpu.c Outdated
@flibitijibibo

Copy link
Copy Markdown
Collaborator

With the sampler state issue fixed, we get something more expected:

In entries[0], binding index 0 not present in the bind group layout.
Expected layout: []
 - While validating [BindGroupDescriptor] against [BindGroupLayout "Sampler Storage Bind Group Layout (Render)"]
 - While calling [Device].CreateBindGroup([BindGroupDescriptor]).

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]
@TheeStickmahn

TheeStickmahn commented Aug 28, 2026

Copy link
Copy Markdown
Author

It's trying to bind a texture to a shader that has no texture bindings? Do we have the WGSL shader source on hand?

@flibitijibibo

Copy link
Copy Markdown
Collaborator

Maybe it's derivative_uniformity that's causing this?

Fragment

diagnostic(off, derivative_uniformity);

var<private> ps_r0 : vec4<f32>;

@group(2u) @binding(1u) var ps_s0_sampler : sampler;

@group(2u) @binding(0u) var ps_s0_image : texture_2d<f32>;

var<private> ps_t0 : vec4<f32>;

var<private> ps_oC0 : vec4<f32>;

fn ShaderFunction4_inner(ps_v0 : vec4<f32>, ps_PointCoordOrTexCoord0 : vec4<f32>) {
  ps_t0 = ps_PointCoordOrTexCoord0.xyzw;
  let v = ps_t0;
  ps_r0 = textureSample(ps_s0_image, ps_s0_sampler, v.xy);
  ps_r0 = (ps_r0 * ps_v0);
  ps_oC0 = ps_r0;
}

@fragment
fn ShaderFunction4(@location(0u) ps_v0 : vec4<f32>, @location(1u) ps_PointCoordOrTexCoord0 : vec4<f32>) -> @location(0u) vec4<f32> {
  ShaderFunction4_inner(ps_v0, ps_PointCoordOrTexCoord0);
  return ps_oC0;
}

Vertex

var<private> vs_oPos : vec4<f32>;

var<private> vs_oD0 : vec4<f32>;

var<private> vs_oT0 : vec4<f32>;

struct tint_symbol_1 {
  tint_symbol : array<vec4<f32>, 4u>,
}

@group(1u) @binding(0u) var<uniform> vs_uniforms : tint_symbol_1;

fn ShaderFunction3_inner(vs_v0 : vec4<f32>, vs_v1 : vec4<f32>, vs_v2 : vec4<f32>) {
  let v = vs_v2;
  let v_1 = dot(v, vs_uniforms.tint_symbol[0i]);
  vs_oPos = vec4<f32>(vec4<f32>(v_1, v_1, v_1, v_1).x, vs_oPos.yzw);
  let v_2 = vs_v2;
  let v_3 = dot(v_2, vs_uniforms.tint_symbol[1i]);
  let v_4 = vs_oPos;
  vs_oPos = vec4<f32>(v_4.x, vec4<f32>(v_3, v_3, v_3, v_3).y, v_4.zw);
  let v_5 = vs_v2;
  let v_6 = dot(v_5, vs_uniforms.tint_symbol[2i]);
  let v_7 = vs_oPos;
  vs_oPos = vec4<f32>(v_7.xy, vec4<f32>(v_6, v_6, v_6, v_6).z, v_7.w);
  let v_8 = vs_v2;
  let v_9 = dot(v_8, vs_uniforms.tint_symbol[3i]);
  vs_oPos = vec4<f32>(vs_oPos.xyz, vec4<f32>(v_9, v_9, v_9, v_9).w);
  let v_10 = vs_v0;
  vs_oD0 = v_10;
  let v_11 = vs_v1;
  vs_oT0 = vec4<f32>(v_11.xy, vs_oT0.zw);
}

struct tint_symbol_2 {
  @builtin(position)
  vs_oPos : vec4<f32>,
  @location(0u)
  vs_oD0 : vec4<f32>,
  @location(1u)
  vs_oT0 : vec4<f32>,
}

@vertex
fn ShaderFunction3(@location(0u) vs_v0 : vec4<f32>, @location(1u) vs_v1 : vec4<f32>, @location(2u) vs_v2 : vec4<f32>) -> tint_symbol_2 {
  ShaderFunction3_inner(vs_v0, vs_v1, vs_v2);
  return tint_symbol_2(vs_oPos, vs_oD0, vs_oT0);
}


#define WEBGPU_INTERNAL_RequiredFeaturesCount 4
#define WEBGPU_INTERNAL_OptionalFeaturesCount 7
#define WEBGPU_INTERNAL_OptionalFeaturesCount 9

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can omit explicit sizes and just use SDL_arraysize(whatever) in loops etc.

Comment thread src/gpu/webgpu/SDL_gpu_webgpu.c Outdated
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];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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…

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this never happened I am great at programming it's always used SDL_GPUVertexBufferDescription::pitch
image.gif
(i fixed it)

@Akaricchi

Akaricchi commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Maybe it's derivative_uniformity that's causing this?

Can't be, you don't even have any branching in that shader. Does it not compile without suppressing the diagnostic?

@Akaricchi

Copy link
Copy Markdown
Contributor

@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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooops should be fixed now

@manuelosorio

Copy link
Copy Markdown

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:

Binding doesn't exist in [BindGroupLayoutInternal "Sampler Storage Bind Group Layout (Render)"].
 - While validating that the entry-point's declaration for @group(2) @binding(2) matches [BindGroupLayoutInternal "Sampler Storage Bind Group Layout (Render)"]
 - While validating the entry-point's compatibility for group 2 with [BindGroupLayoutInternal "Sampler Storage Bind Group Layout (Render)"]
 - While validating fragment stage ([ShaderModule (unlabeled)], entryPoint: "main").
 - While validating fragment state.
 - While calling [Device].CreateRenderPipeline([RenderPipelineDescriptor]).

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 WebGPUShaderBindGroupLayouts from checking for an array to checking for var<storage. It resulted in the original shader and the version I manually overrode to load with issues in the browser. It might be similar with the compute shader binding.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.