unoplatform/uno · critical · Exception

Readback FBO is not complete

Error message

Readback FBO is not complete

What it means

Thrown by GLCanvasElement_PixelBuffersElement.Init when gl.CheckFramebufferStatus returns anything other than GL_FRAMEBUFFER_COMPLETE after attaching the readback texture (InternalFormat.Rgba8, 64x64) to a framebuffer. It means the GL driver considers the FBO unusable for rendering/readback, so the byte-exact PBO round-trip self-check that follows cannot proceed. This is a hard precondition: a non-complete FBO makes gl.ReadPixels undefined.

Source

Thrown at src/SamplesApp/SamplesApp.Samples/Windows_UI_Composition/PixelBuffersGlCanvasElement.cs:69

			gl.BindBuffer(BufferTargetARB.PixelUnpackBuffer, _unpackPbo);
			gl.BufferData(BufferTargetARB.PixelUnpackBuffer, new ReadOnlySpan<byte>(pixels), BufferUsageARB.StreamDraw);

			_texture = gl.GenTexture();
			gl.BindTexture(TextureTarget.Texture2D, _texture);
			gl.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba8, TexSize, TexSize, 0, GLEnum.Rgba, GLEnum.UnsignedByte, (void*)0);
			gl.TexParameter(TextureTarget.Texture2D, GLEnum.TextureMinFilter, (int)GLEnum.Nearest);
			gl.TexParameter(TextureTarget.Texture2D, GLEnum.TextureMagFilter, (int)GLEnum.Nearest);
			gl.TexParameter(TextureTarget.Texture2D, GLEnum.TextureWrapS, (int)GLEnum.ClampToEdge);
			gl.TexParameter(TextureTarget.Texture2D, GLEnum.TextureWrapT, (int)GLEnum.ClampToEdge);
			gl.BindBuffer(BufferTargetARB.PixelUnpackBuffer, 0);

			// --- Readback path: texture -> FBO -> ReadPixels -> pack PBO ---
			_fbo = gl.GenFramebuffer();
			gl.BindFramebuffer(GLEnum.Framebuffer, _fbo);
			gl.FramebufferTexture2D(GLEnum.Framebuffer, FramebufferAttachment.ColorAttachment0, GLEnum.Texture2D, _texture, 0);
			if (gl.CheckFramebufferStatus(GLEnum.Framebuffer) != GLEnum.FramebufferComplete)
			{
				throw new Exception("Readback FBO is not complete");
			}

			_packPbo = gl.GenBuffer();
			gl.BindBuffer(BufferTargetARB.PixelPackBuffer, _packPbo);
			gl.BufferData(BufferTargetARB.PixelPackBuffer, (nuint)pixels.Length, null, BufferUsageARB.StreamRead);
			gl.ReadPixels(0, 0, TexSize, TexSize, GLEnum.Rgba, GLEnum.UnsignedByte, (void*)0);
			gl.BindFramebuffer(GLEnum.Framebuffer, 0);

			// --- Round-trip self-check: pack PBO -> CPU, byte-exact against the source ---
			var readBack = new byte[pixels.Length];
			if (OperatingSystem.IsAndroid() || OperatingSystem.IsIOS())
			{
				// Native GLES has no glGetBufferSubData; map the pack PBO and copy it out.
				var mapped = (byte*)gl.MapBufferRange(BufferTargetARB.PixelPackBuffer, 0, (nuint)readBack.Length, MapBufferAccessMask.ReadBit);
				if (mapped is null)
				{
					throw new Exception("MapBufferRange(PixelPackBuffer) returned null");
				}

View on GitHub (pinned to 0418340488)

Solutions

  1. Call gl.GetError() right before the CheckFramebufferStatus to surface the real GL error code that made the texture/FBO incomplete (0x502 invalid operation, 0x501 invalid enum, etc.).
  2. Query the actual GL context via gl.GetString(StringName.Version) and StringName.ShadingLanguageVersion to confirm whether RGBA8 color-renderable targets are supported on this device; on pure GLES2 swap InternalFormat.Rgba8 for a format the driver advertises as renderable.
  3. Verify the texture is complete on its own: ensure TexImage2D ran with non-zero dimensions and, if the min filter were Mipmap-based, that all mip levels exist (here it is Nearest, so that is not the cause, but confirm no prior bind overwrote _texture).
  4. Check the completeness code explicitly (FramebufferStatus.Unsupported, IncompleteAttachment, IncompleteMissingAttachment) instead of only 'not complete' to pinpoint whether the attachment, dimensions, or format is the offender.

Example fix

// before
if (gl.CheckFramebufferStatus(GLEnum.Framebuffer) != GLEnum.FramebufferComplete)
{
    throw new Exception("Readback FBO is not complete");
}

// after — surface the specific completeness code and any pending GL error
var fbErr = gl.GetError();
var status = gl.CheckFramebufferStatus(GLEnum.Framebuffer);
if (status != GLEnum.FramebufferComplete)
{
    throw new Exception($"Readback FBO not complete: status=0x{(int)status:x}, pending GL error=0x{(int)fbErr:x}");
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate the texture is renderable-complete before attaching it to the FBO
gl.BindTexture(TextureTarget.Texture2D, _texture);
gl.GetTexLevelParameter(TextureTarget.Texture2D, 0, GetTextureParameter.TextureWidth, out int w);
gl.GetTexLevelParameter(TextureTarget.Texture2D, 0, GetTextureParameter.TextureHeight, out int h);
if (w == 0 || h == 0) return; // texture has no storage; FBO attach will be incomplete
var pending = gl.GetError();
if (pending != GLEnum.NoError) return; // earlier error corrupted state

Try / catch

// Wrap the whole Init so an FBO failure does not kill the host app
try { Init(gl); }
catch (Exception ex) when (ex.Message.Contains("FBO is not complete"))
{
    _initFailed = true;
    App.MainWindow?.LogError($"PixelBuffers FBO incomplete: {ex.Message}");
}

Prevention

When it happens

Trigger: Specifically after gl.FramebufferTexture2D(Framebuffer, ColorAttachment0, Texture2D, _texture, 0) followed by gl.CheckFramebufferStatus(GLEnum.Framebuffer) != GLEnum.FramebufferComplete (line 67). The attached _texture was created with InternalFormat.Rgba8 and TexImage2D sized TexSize=64, Nearest min/mag filter (no mipmaps required).

Common situations: The GL context is OpenGL ES / WebGL2 where RGBA8 color-renderable support is missing or the sized internal format is rejected; the texture's TexImage2D storage was never actually allocated (an earlier GL error left _texture incomplete); running on a software/llvmpipe rasterizer that reports limited framebuffer configs; a previous un-queried GL error corrupted texture state before the FBO attach.

Related errors


AI-assisted analysis of unoplatform/uno@0418340488 (2026-08-13). Data as JSON: /api/errors/e62a10a1d281355c. Report an issue: GitHub.