#5978·raylib

[rlsw] `LoadRenderTexture()` framebuffer is `INCOMPLETE_ATTACHMENT` unless `SW_FRAMEBUFFER_DEPTH_TYPE == D32`

Author: HurricanVDCreated Jul 13, 2026Updated Jul 16, 2026

Issue description

With the software renderer (GRAPHICS_API_OPENGL_SOFTWARE), LoadRenderTexture() produces an incomplete framebuffer whenever rlsw is compiled with a depth type other than the default D32 (i.e. SW_FRAMEBUFFER_DEPTH_TYPE = D8 or D16). rlFramebufferComplete() logs WARNING: FBO: [ID 1] Framebuffer has incomplete attachment and any BeginTextureMode()/EndTextureMode() then renders into nothing (the render texture stays empty). The default D32 build works, so the bug is latent and only appears once you pick a smaller depth buffer (e.g. to save memory on a constrained target).

Root cause

The rlsw rasterizer is bound at compile time to exactly one depth format, SW_FRAMEBUFFER_DEPTH_FORMAT (from SW_FRAMEBUFFER_DEPTH_TYPE); depth is read/written via the compile-time SW_FRAMEBUFFER_DEPTH_GET/SET macros with a fixed stride. So every depth attachment must have that format, and swCheckFramebufferStatus() enforces it:

c
// external/rlsw.h - swCheckFramebufferStatus()
if (RLSW.depthBuffer->format != SW_FRAMEBUFFER_DEPTH_FORMAT)
    return SW_FRAMEBUFFER_INCOMPLETE_ATTACHMENT;

But the FBO depth attachment is created with a fixed format that ignores the compiled type, at two spots:

1. src/rlgl.h, rlLoadTextureDepth() software branch hard-codes GL_DEPTH_COMPONENT32 (its own comment even warns about it):

c
#elif defined(GRAPHICS_API_OPENGL_SOFTWARE)
    // NOTE: Renderbuffers are the same type of object as textures in rlsw
    // WARNING: Ensure that the depth format is the one specified at rlsw compilation
    glGenRenderbuffers(1, &id);
    glBindRenderbuffer(GL_RENDERBUFFER, id);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT32, width, height); // hard-coded
    glBindRenderbuffer(GL_RENDERBUFFER, 0);
#endif

2. src/external/rlsw.h, swTexStorage2D() maps every depth internal format to D16/D32 regardless of the compiled type (there is no GL_DEPTH_COMPONENT8 enum):

c
case SW_DEPTH_COMPONENT16:  pixelFormat = SW_PIXELFORMAT_DEPTH_D16; break;
case SW_DEPTH_COMPONENT24:  pixelFormat = SW_PIXELFORMAT_DEPTH_D32; break;
case SW_DEPTH_COMPONENT32:  pixelFormat = SW_PIXELFORMAT_DEPTH_D32; break;
case SW_DEPTH_COMPONENT32F: pixelFormat = SW_PIXELFORMAT_DEPTH_D32; break;

On a D8 build the FBO depth attachment ends up D32, so D32 != D8 -> INCOMPLETE_ATTACHMENT. On the default D32 build the two happen to agree, which is why the default build works.

Steps to reproduce

Build rlsw with a non-default depth type and create a render texture:

c
// rlsw compiled with -DSW_FRAMEBUFFER_DEPTH_TYPE=D8  (or D16)
InitWindow(800, 450, "rt");
RenderTexture2D t = LoadRenderTexture(160, 90);
// console: "WARNING: FBO: [ID 1] Framebuffer has incomplete attachment"

The stock examples/core/core_smooth_pixelperfect then shows an empty canvas (text overlays draw fine; the up-scaled render-texture region stays blank).

Minimal engine-free reproduction (runs on any host). rlsw is pure software, so the completeness path can be driven without a window/GL: #define RLSW_IMPLEMENTATION, swInit(), then create a color texture + a depth renderbuffer via glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT32, ...), attach both, and print glCheckFramebufferStatus() - i.e. exactly the gl* -> sw* calls rlgl makes. Built with native x86_64 mingw-gcc against the unmodified upstream rlsw.h, changing only SW_FRAMEBUFFER_DEPTH_TYPE:

SW_FRAMEBUFFER_DEPTH_TYPE framebuffer status
D8 0x8cd6 INCOMPLETE_ATTACHMENT
D16 0x8cd6 INCOMPLETE_ATTACHMENT
D32 (default) 0x8cd5 COMPLETE

Proposed fix

Any depth attachment used by the rasterizer must have the one compile-time format (SW_FRAMEBUFFER_DEPTH_FORMAT) - depth uses the fixed-stride SW_FRAMEBUFFER_DEPTH_GET/SET macros, so a different-format depth buffer would also be misread, not merely incomplete. Two ways:

Option A - minimal (rlsw only). Coerce any depth request in swTexStorage2D() to the compiled format:

c
case SW_DEPTH_COMPONENT16:
case SW_DEPTH_COMPONENT24:
case SW_DEPTH_COMPONENT32:
case SW_DEPTH_COMPONENT32F: pixelFormat = SW_FRAMEBUFFER_DEPTH_FORMAT; break;

One line, one file, covers D8 (no GL enum exists for it); default D32 unchanged.

Option B - explicit format (rlsw + rlgl). If you prefer the format named rather than silently coerced: add a (non-standard) SW_DEPTH_COMPONENT8 to SWinternalformat (the internal SW_PIXELFORMAT_DEPTH_D8 already exists) and map it in swTexStorage2D(); then in rlgl.h rlLoadTextureDepth() (software) replace the hard-coded GL_DEPTH_COMPONENT32 with the enum matching the compiled SW_FRAMEBUFFER_DEPTH_TYPE. More faithful, but touches two files, needs a non-standard enum, and adds no capability (the rasterizer is single-format, so rlLoadTextureDepth must request exactly the compiled format anyway).

Either way, keep the depthBuffer->format != SW_FRAMEBUFFER_DEPTH_FORMAT check in swCheckFramebufferStatus() as the backstop. (I used Option A in my port and verified it - table above.)

Environment

  • raylib 6.1-dev @ f00317be
  • GRAPHICS_API_OPENGL_SOFTWARE (rlsw), SW_FRAMEBUFFER_DEPTH_TYPE = D8
  • Found while porting raylib to AmigaOS 3.2 (68k, software renderer); the minimal repro above was run and confirmed on Windows x86_64 (mingw-gcc 15.2) against the unmodified upstream rlsw.h.