I encountered this issue while implementing MTSDF atlases. Codex prepared a fix for me, but I don’t want to open a pull request because I’m not confident about the code or the patch. Here is what Codex suggests to fix the problem:
Description
On Apple platforms, images decoded by createImageBitmap are always stored with premultiplied RGB channels.
Additionally:
createImageBitmap ignores ImageBitmapOptions.premultiplyAlpha.
GPUQueue.copyExternalImageToTexture ignores GPUImageCopyTextureTagged.premultipliedAlpha.
- The same behavior occurs when
flipY is enabled.
This makes it impossible to upload straight/unpremultiplied RGBA data without losing information.
Current behavior
The Apple implementation creates the bitmap using:
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big
Core Graphics therefore transforms decoded pixels as follows:
RGB_stored = RGB_source × alpha
For example, a source pixel containing:
RGBA = [128, 128, 128, 128]
is decoded approximately as:
The original straight RGB values are no longer available after quantization.
copyExternalImageToTexture then uploads these bytes directly through WriteTexture, regardless of the value of:
destination.premultipliedAlpha
Expected behavior
createImageBitmap
The native implementation should respect:
createImageBitmap(source, {
premultiplyAlpha: "none"
})
When "none" is requested, the resulting ImageBitmap should preserve straight RGBA values:
When "premultiply" is requested, the resulting values should be premultiplied:
The bitmap should retain enough metadata to identify whether its stored data is premultiplied.
copyExternalImageToTexture
GPUQueue.copyExternalImageToTexture should convert between the source representation and the representation requested by:
destination.premultipliedAlpha
The default value of GPUImageCopyTextureTagged.premultipliedAlpha must remain false.
The conversion should happen exactly once:
| Source representation |
Destination value |
Expected operation |
| Straight |
false or omitted |
No conversion |
| Straight |
true |
Premultiply |
| Premultiplied |
false or omitted |
Unpremultiply |
| Premultiplied |
true |
No conversion |
The same alpha semantics should apply when source.flipY is enabled.
Why decoding straight RGBA matters
Unpremultiplying an already quantized 8-bit pixel cannot always recover the original RGB values, especially when alpha is low.
For data textures, RGB may also be independent of alpha. One example is an MTSDF texture, where RGB and alpha store separate distance fields rather than color opacity.
Premultiplying this data corrupts the RGB distance field and changes the rendered glyph contour.
This issue is not specific to fonts: any texture that requires independent RGB and alpha channels is affected.
Minimal reproduction
Use a PNG containing a semitransparent pixel whose RGB values are independent of alpha:
RGBA = [128, 128, 128, 128]
Create the bitmap without premultiplication:
const bitmap = await createImageBitmap(blob, {
premultiplyAlpha: "none",
});
Upload it with the destination option omitted:
device.queue.copyExternalImageToTexture(
{ source: bitmap },
{ texture },
[1, 1],
);
Read the texture back.
Expected
Actual on Apple platforms
Approximately:
The following cases should also be tested:
createImageBitmap with premultiplyAlpha: "none".
createImageBitmap with premultiplyAlpha: "premultiply".
copyExternalImageToTexture with premultipliedAlpha omitted.
copyExternalImageToTexture with premultipliedAlpha: true.
- The same copies with
flipY: true.
- Premultiplied sources, to verify that no double premultiplication occurs.
- Straight sources, to verify that no unnecessary unpremultiplication occurs.
Suggested implementation
On Apple platforms:
- Decode images directly into straight RGBA when possible, avoiding lossy unpremultiplication after 8-bit quantization.
- Store whether each
ImageBitmap contains straight or premultiplied data.
- Respect
ImageBitmapOptions.premultiplyAlpha when constructing the bitmap.
- In
copyExternalImageToTexture, compare the source representation with destination.premultipliedAlpha.
- Convert only when the representations differ.
- Apply the same conversion rules in both the normal and
flipY paths.
Affected files
The relevant implementation appears to be located in:
packages/webgpu/apple/ApplePlatformContext.mm
packages/webgpu/cpp/rnwgpu/PlatformContext.h
packages/webgpu/cpp/rnwgpu/api/ImageBitmap.h
packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h
packages/webgpu/cpp/rnwgpu/api/GPUQueue.cpp
I encountered this issue while implementing MTSDF atlases. Codex prepared a fix for me, but I don’t want to open a pull request because I’m not confident about the code or the patch. Here is what Codex suggests to fix the problem:
Description
On Apple platforms, images decoded by
createImageBitmapare always stored with premultiplied RGB channels.Additionally:
createImageBitmapignoresImageBitmapOptions.premultiplyAlpha.GPUQueue.copyExternalImageToTextureignoresGPUImageCopyTextureTagged.premultipliedAlpha.flipYis enabled.This makes it impossible to upload straight/unpremultiplied RGBA data without losing information.
Current behavior
The Apple implementation creates the bitmap using:
Core Graphics therefore transforms decoded pixels as follows:
For example, a source pixel containing:
is decoded approximately as:
The original straight RGB values are no longer available after quantization.
copyExternalImageToTexturethen uploads these bytes directly throughWriteTexture, regardless of the value of:Expected behavior
createImageBitmapThe native implementation should respect:
When
"none"is requested, the resultingImageBitmapshould preserve straight RGBA values:When
"premultiply"is requested, the resulting values should be premultiplied:The bitmap should retain enough metadata to identify whether its stored data is premultiplied.
copyExternalImageToTextureGPUQueue.copyExternalImageToTextureshould convert between the source representation and the representation requested by:The default value of
GPUImageCopyTextureTagged.premultipliedAlphamust remainfalse.The conversion should happen exactly once:
falseor omittedtruefalseor omittedtrueThe same alpha semantics should apply when
source.flipYis enabled.Why decoding straight RGBA matters
Unpremultiplying an already quantized 8-bit pixel cannot always recover the original RGB values, especially when alpha is low.
For data textures, RGB may also be independent of alpha. One example is an MTSDF texture, where RGB and alpha store separate distance fields rather than color opacity.
Premultiplying this data corrupts the RGB distance field and changes the rendered glyph contour.
This issue is not specific to fonts: any texture that requires independent RGB and alpha channels is affected.
Minimal reproduction
Use a PNG containing a semitransparent pixel whose RGB values are independent of alpha:
Create the bitmap without premultiplication:
Upload it with the destination option omitted:
Read the texture back.
Expected
Actual on Apple platforms
Approximately:
The following cases should also be tested:
createImageBitmapwithpremultiplyAlpha: "none".createImageBitmapwithpremultiplyAlpha: "premultiply".copyExternalImageToTexturewithpremultipliedAlphaomitted.copyExternalImageToTexturewithpremultipliedAlpha: true.flipY: true.Suggested implementation
On Apple platforms:
ImageBitmapcontains straight or premultiplied data.ImageBitmapOptions.premultiplyAlphawhen constructing the bitmap.copyExternalImageToTexture, compare the source representation withdestination.premultipliedAlpha.flipYpaths.Affected files
The relevant implementation appears to be located in: