From b7660ed48c6251d8d55d708d9992dd19c761fb89 Mon Sep 17 00:00:00 2001 From: Ivan Ushakov Date: Sat, 8 Aug 2026 11:41:04 +0300 Subject: [PATCH] Don't ask D3D12 for anisotropic filtering with point mipmapping Windows 10 users crashed at FT::Font::size() with a null this. The font was the last thing to go wrong, not the first: the whole GPU device was already dead. SDLWaterRenderer reuses one SDL_GPUSamplerCreateInfo for both its samplers. The second switches mipmap_mode to NEAREST for a render target with no mip chain, but enable_anisotropy is still set from the first. Vulkan and Metal carry the filters and anisotropy as independent fields and take that happily. D3D12 packs them into one enum, and SDLToD3D12_Filter builds it arithmetically: MIN_MAG_LINEAR_MIP_POINT (0x14) | ANISOTROPIC (0x40) = 0x54, which is not a D3D12_FILTER. Only 0x55 and its comparison form are. CreateSampler returns void, so the runtime cannot refuse it by return code. It removes the device instead, and every texture and pipeline after that fails with DXGI_ERROR_INVALID_CALL: the water and minimap pipelines, then the glyph atlas, whose failure surfaced two subsystems away as a null FT::Font that nothing checks. Windows 11 hid all of it -- its newer D3D12Core.dll sanitises the bit pattern, most likely to the D3D12_FILTER_ANISOTROPIC this now asks for outright. Dropping the line is behaviour-preserving: with one mip level there is nothing for POINT and LINEAR to differ over, and max_lod already pins sampling to it. Swept the other nine sampler sites; anisotropy is either off or already paired with linear mipmapping. The rule is written up in Build-PORTING.md beside the vertex-semantic trap, which is the same shape of problem. Co-Authored-By: Claude Opus 5 --- Documents/Build-PORTING.md | 30 ++++++++++++++++++++++++++++++ Render/SDLWaterRenderer.cpp | 12 +++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Documents/Build-PORTING.md b/Documents/Build-PORTING.md index 5d7c3c86..5dfd3d1e 100644 --- a/Documents/Build-PORTING.md +++ b/Documents/Build-PORTING.md @@ -62,6 +62,36 @@ uniforms at `b0, space1`, fragment textures/samplers at `t#/s#, space2`, fragmen at `b0, space3`. Getting one wrong fails pipeline creation the same opaque way, because the root signature SDL builds will not cover the shader's bindings. +### Anisotropic sampling forces linear min/mag/mip + +Same shape of trap, one layer down. Vulkan and Metal carry the three filters and anisotropy +as independent fields; D3D12 packs them into one `D3D12_FILTER` enum, and anisotropy is a bit +set on top of an all-linear encoding. `SDLToD3D12_Filter` builds it arithmetically, so + +```c +enable_anisotropy = true; mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST; +``` + +encodes `MIN_MAG_LINEAR_MIP_POINT (0x14) | ANISOTROPIC (0x40)` = `0x54`, **which is not a +member of `D3D12_FILTER`**. Only `0x55` (`D3D12_FILTER_ANISOTROPIC`) and `0xd5` (its +comparison form) are legal. So: if `enable_anisotropy` is set, all three of `min_filter`, +`mag_filter` and `mipmap_mode` must be linear. + +Getting it wrong is not a returned error. `CreateSampler` returns `void`, so the runtime +answers an unrecognised filter by **removing the device** — after which every texture and +pipeline fails with `DXGI_ERROR_INVALID_CALL` and nothing points back at the sampler: + +``` +D3D12 ERROR: ID3D12Device::CreateSampler: Filter unrecognized. [ STATE_CREATION ERROR #742 ] +D3D12: Removing Device. +D3D12 ERROR: ID3D12Device::RemoveDevice: ... [ EXECUTION ERROR #232: DEVICE_REMOVAL_PROCESS_AT_FAULT ] +``` + +Watch for a `SDL_GPUSamplerCreateInfo` **reused** between samplers: that is how the water +renderer got one, by switching `mipmap_mode` for a second sampler while `enable_anisotropy` +stayed set from the first. Windows 11 hid it — its newer `D3D12Core.dll` sanitises the bit +pattern where Windows 10's treats it as an illegal call. + ## One source list per module Every module's `CMakeLists.txt` used to have a `WIN32` branch feeding it a different set of diff --git a/Render/SDLWaterRenderer.cpp b/Render/SDLWaterRenderer.cpp index dc1b13c3..371e9e1b 100644 --- a/Render/SDLWaterRenderer.cpp +++ b/Render/SDLWaterRenderer.cpp @@ -74,7 +74,17 @@ void SDLWaterRenderer::createPipelines() si.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; si.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; si.max_lod = 0.f; // the render target has no mip chain - si.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST; + // mipmap_mode stays LINEAR, and must: si still carries enable_anisotropy from the + // sampler above, and D3D12 encodes anisotropy as a bit on top of the min/mag/mip + // filter -- MIN_MAG_LINEAR_MIP_POINT | ANISOTROPIC is 0x54, which is not a member of + // D3D12_FILTER. CreateSampler cannot fail by return code, so the runtime answers an + // unrecognised filter by REMOVING THE DEVICE, and every texture and pipeline after it + // fails with DXGI_ERROR_INVALID_CALL -- including the font atlas, which is where it + // finally surfaced, as a null FT::Font several subsystems away. + // Vulkan and Metal carry the filters and anisotropy as independent fields, so the same + // call is legal there and this only ever reached Windows. + // Nothing is lost: with one mip level there is nothing for POINT and LINEAR to differ + // over, and max_lod above already pins sampling to it. samplerClamp_ = SDL_CreateGPUSampler(device_, &si); // 1x1 flat wave map: the decoder's bias, so water.frag.hlsl's slope() reads 0.