Skip to content

Fix half-float render target fallback for mobile GPUs missing EXT_color_buffer_half_float#33114

Draft
RodrigoHamuy wants to merge 7 commits intomrdoob:devfrom
RodrigoHamuy:fix/mobile-ocean-example
Draft

Fix half-float render target fallback for mobile GPUs missing EXT_color_buffer_half_float#33114
RodrigoHamuy wants to merge 7 commits intomrdoob:devfrom
RodrigoHamuy:fix/mobile-ocean-example

Conversation

@RodrigoHamuy
Copy link
Contributor

@RodrigoHamuy RodrigoHamuy commented Mar 3, 2026

On devices that support WebGL 2.0 but lack EXT_color_buffer_half_float (e.g. Google Pixel 6 Pro), rendering into HalfFloatType render targets silently fails. PMREMGenerator and Water were both hardcoded to HalfFloatType. Both now fall back to FloatType when the extension is unavailable.

Steps to reproduce

  • Steps: Open webgl_shaders_ocean example on a device without EXT_color_buffer_half_float (e.g. Google Pixel 6 Pro) and set elevation to 30 so the sun is visible.
  • Expected: Should look okey.
  • Before It looked like this (video in details)
image
video
screen-20260303-225610-1772578560095.mp4
  • After:
Screenshot_20260303-232404

Potential next steps

The same extension guard is missing in several other WebGL render targets. They will silently produce black or broken output on the same class of devices:

File Context
src/renderers/webgl/WebGLShadowMap.js VSM shadow map buffers — breaks all VSM shadows
examples/jsm/objects/Reflector.js Mirror reflection render target
examples/jsm/objects/Refractor.js Refraction render target
examples/jsm/objects/ReflectorForSSRPass.js SSR reflector render target
examples/jsm/postprocessing/EffectComposer.js Main composer buffers — breaks most post-processing
examples/jsm/postprocessing/UnrealBloomPass.js Bloom
examples/jsm/postprocessing/OutlinePass.js 6 render targets
examples/jsm/postprocessing/SSAOPass.js SSAO
examples/jsm/postprocessing/GTAOPass.js GTAO
examples/jsm/postprocessing/SAOPass.js SAO
examples/jsm/postprocessing/SMAAPass.js SMAA
examples/jsm/postprocessing/AfterimagePass.js Afterimage
examples/jsm/postprocessing/BloomPass.js Bloom (legacy)
examples/jsm/postprocessing/SavePass.js Save pass
examples/jsm/postprocessing/BokehPass.js Bokeh
examples/jsm/postprocessing/TAARenderPass.js TAA
examples/jsm/postprocessing/SSAARenderPass.js SSAA
examples/jsm/postprocessing/SSRPass.js SSR
examples/jsm/postprocessing/RenderTransitionPass.js Transition
examples/jsm/postprocessing/RenderPixelatedPass.js Pixelated
src/renderers/webgl/WebGLOutput.js Intermediate tone-mapping buffer (only active when user opts into non-UnsignedByteType output)

Note: WebGLRenderer itself is already guarded (line 1950). All WebGPU/TSL paths (src/nodes/*, src/renderers/common/*) and loaders (EXRLoader, HDRLoader, etc.) use HalfFloatType as a sampled DataTexture, which requires a different — universally supported — extension and is not affected.

@RodrigoHamuy RodrigoHamuy marked this pull request as draft March 3, 2026 22:50
@github-actions
Copy link

github-actions bot commented Mar 3, 2026

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 359.27
85.31
359.58
85.42
+309 B
+112 B
WebGPU 629.93
174.99
629.93
174.99
+0 B
+0 B
WebGPU Nodes 628.51
174.74
628.51
174.74
+0 B
+0 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 491.03
119.72
491.33
119.85
+302 B
+130 B
WebGPU 703.55
189.99
703.55
189.99
+0 B
+0 B
WebGPU Nodes 652.76
177.37
652.76
177.37
+0 B
+0 B

@RodrigoHamuy RodrigoHamuy changed the title Fix half-float render target fallback for mobile GPUs missing EXT_color_buffer_half_float Fix half-float render target fallback for mobile GPUs missing EXT_color_buffer_half_float Mar 3, 2026
@RodrigoHamuy RodrigoHamuy marked this pull request as ready for review March 3, 2026 23:13
@RodrigoHamuy
Copy link
Contributor Author

I would be happy to extend the guard, since it's quite an easy and rewarding fix 😄 but just would like confirmation if that's the right approach to take? We could still merge this and deal with the rest on another PR?

@mrdoob
Copy link
Owner

mrdoob commented Mar 4, 2026

Maybe the renderer could do this automatically? (While logging a warning in the console)


scope.onBeforeRender = function ( renderer, scene, camera ) {

// Lazily fall back to FloatType if EXT_color_buffer_half_float is unsupported (e.g. some mobile GPUs)
Copy link
Collaborator

@Mugen87 Mugen87 Mar 4, 2026

Choose a reason for hiding this comment

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

I would expect a fallback to UnsignedByteType if half float isn't available. The render target setup for transmission works like that:

const hasHalfFloatSupport = extensions.has( 'EXT_color_buffer_half_float' ) || extensions.has( 'EXT_color_buffer_float' );
currentRenderState.state.transmissionRenderTarget[ camera.id ] = new WebGLRenderTarget( 1, 1, {
generateMipmaps: true,
type: hasHalfFloatSupport ? HalfFloatType : UnsignedByteType,
minFilter: LinearMipmapLinearFilter,
samples: Math.max( 4, capabilities.samples ), // to avoid feedback loops, the transmission render target requires a resolve, see #26177
stencilBuffer: stencil,
resolveDepthBuffer: false,
resolveStencilBuffer: false,
colorSpace: ColorManagement.workingColorSpace,
} );

Copy link
Collaborator

@Mugen87 Mugen87 Mar 4, 2026

Choose a reason for hiding this comment

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

To clarify: There is no need to use FloatType. We can keep using HalfFloatType but just request EXT_color_buffer_float instead. This extension includes formats like gl.RGBA16F.

Reference: https://registry.khronos.org/webgl/extensions/EXT_color_buffer_float/

The thing is, the renderer already requests EXT_color_buffer_float here:

getExtension( 'EXT_color_buffer_float' );
getExtension( 'WEBGL_clip_cull_distance' );
getExtension( 'OES_texture_float_linear' );
getExtension( 'EXT_color_buffer_half_float' );
getExtension( 'WEBGL_multisampled_render_to_texture' );
getExtension( 'WEBGL_render_shared_exponent' );

I'm now confused why the changes in the PR are required in the first place.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My phone has the following capabilities:

❌ EXT_color_buffer_half_float
✅ EXT_color_buffer_float
✅ OES_texture_float_linear

If I replace FloatType with UnsignedByteType as fallback, I get darker brights

With PR as it is

Image

With UnsignedByteType fallback

Image

What would you like me to do?

Copy link
Collaborator

@Mugen87 Mugen87 Mar 4, 2026

Choose a reason for hiding this comment

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

If EXT_color_buffer_float is supported on your device, HalfFloatType should work. If it doesn't, we must find out the root cause.

Do you see any console warnings when HalfFloatType is used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No console warnings, silently passes. Driver issue?

Btw, I moved the fallback to WebGLTextures as it seems like the right place instead as suggested by @mrdoob .

Copy link
Collaborator

@Mugen87 Mugen87 Mar 9, 2026

Choose a reason for hiding this comment

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

It seems the issue was introduced with #32680 which has added bloom to the demo.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The pixels in webgl_shaders_ocean can get very bright due to how the sky is implemented now (it does not use a built-in gamma correction since r183). If you then generate a PMREM from the scene and use it as an environment map, the objects in your scene end up bright as well. If you then add bloom to the scene, it further increases the luminance. I think this gets to a point where certain devices can't handle the values anymore.

Given that there is no issues on Desktop and the demo uses RGBAF16 on all devices, the root cause must be related to something device specific.

Copy link
Contributor Author

@RodrigoHamuy RodrigoHamuy Mar 10, 2026

Choose a reason for hiding this comment

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

I think the issue in r183 happens even if I disable bloom (which I did while debugging this).
r182 does work fine, although, damn, that was a nice upgrade in the last version 😄 . The new sky looks so much better!!

Copy link
Collaborator

@Mugen87 Mugen87 Mar 10, 2026

Choose a reason for hiding this comment

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

Indeed, without bloom the rendering breaks but more subtle. Here is a test link right before the bloom addition:

https://rawcdn.githack.com/mrdoob/three.js/98a7dd547f988ad0cfaf697ec41a0ff5bf254d0b/examples/webgl_shaders_ocean.html

With a high elevation, the box mesh becomes black (it should turn white though). Nevertheless, bloom seems to amplify the issue. The breakage on mobile starts with #32677 though.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we must somehow isolate this issue into a smaller test case. Like the one from #33114 (comment). In this way, it will be easier to get feedback from the browser dev side.

…rted devices

When EXT_color_buffer_half_float is unavailable, setupRenderTarget now
automatically falls back to FloatType (if EXT_color_buffer_float is present)
or UnsignedByteType, emitting a one-time console warning. This covers Water,
PMREMGenerator, shadow maps, post-processing passes, and all other WebGL
render targets uniformly.

Revert per-component patches in Water.js and PMREMGenerator.js — now redundant.
@mrdoob mrdoob added this to the r184 milestone Mar 4, 2026
@Mugen87 Mugen87 marked this pull request as draft March 12, 2026 11:37
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.

4 participants