Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,43 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.30.26] - 2026-07-30
## [0.30.27] - 2026-07-30

### Fixed

- **GLES: depth/stencil not attached to swapchain FBO on surface render pass** —
`setupSurfaceTarget()` had an early return that skipped depth/stencil attachment.
3D content with depth testing was invisible on GLES while Vulkan and DX12 worked
correctly. Now attaches depth/stencil to the swapchain FBO via
`AttachDepthStencilToFBOCommand`. Matches Rust wgpu-hal GLES `begin_render_pass`
which uses the same draw_fbo path for both surface and offscreen targets. (#284)
`setupSurfaceTarget()` had an early return that skipped depth/stencil attachment,
causing `GL_INVALID_FRAMEBUFFER_OPERATION` (0x506) on every draw call.
Now attaches depth/stencil to the swapchain FBO via `AttachDepthStencilToFBOCommand`.
Attachment point chosen by format: `GL_DEPTH_ATTACHMENT` for depth-only,
`GL_DEPTH_STENCIL_ATTACHMENT` for combined formats (Rust wgpu-hal parity,
command.rs:577-580). Also fixes the same wrong attachment point in the existing
`AttachDepthStencilCommand` for offscreen FBOs. 2D overlay now renders correctly
on GLES surface targets. (#284)

- **GLES: MappedAtCreation buffer data silently discarded on Unmap** —
`UnmapBuffer` only flushed shadow data to GL when `BufferUsageMapWrite` was set.
Per WebGPU spec, `MappedAtCreation` does NOT require `MapWrite` usage. Buffers
created with `Uniform|CopyDst` + `MappedAtCreation` (the standard g3d pattern)
had their data thrown away, leaving GL buffers zero-filled — zero MVP matrices,
zero vertices, zero indices. Root cause of invisible 3D geometry on GLES. (#284)

- **GLES: 3D geometry invisible due to stale depth mask** — `ClearDepthCommand`
did not call `glDepthMask(true)` before `glClear(GL_DEPTH_BUFFER_BIT)`. If a
prior pipeline set `DepthWriteEnabled=false`, the depth clear was silently
masked on subsequent frames, causing all 3D geometry to fail the depth test.
Also adds `glClearDepth(value)` before clear (was relying on GL default 1.0).
Rust ref: queue.rs:1199-1205. (#284)

- **GLES: viewport depth range ignored** — `SetViewportCommand` called
`glViewport` but not `glDepthRange(minDepth, maxDepth)`. Depth range fields
were stored but never passed to GL. Rust ref: queue.rs:1295-1296. (#284)

### Added

- `gl.Context.ClearDepth()` and `gl.Context.DepthRange()` — wrapper methods for
both Windows (syscall, double) and Linux (goffi, float32 for GLES). Function
pointers were loaded via `getProcAddr` but had no callable methods.

### Changed

Expand Down
43 changes: 32 additions & 11 deletions hal/gles/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,9 @@ func (e *CommandEncoder) setupSurfaceTarget(desc *hal.RenderPassDescriptor, tv *
cfg := surf.config
rpe.fbHeight = cfg.Height
e.commands = append(e.commands, &SetViewportCommand{
width: float32(cfg.Width),
height: float32(cfg.Height),
width: float32(cfg.Width),
height: float32(cfg.Height),
maxDepth: 1,
})
}
// Attach depth/stencil to the swapchain FBO if requested.
Expand Down Expand Up @@ -364,8 +365,9 @@ func (e *CommandEncoder) setupOffscreenTarget(

rpe.fbHeight = tv.texture.size.Height
e.commands = append(e.commands, &SetViewportCommand{
width: float32(tv.texture.size.Width),
height: float32(tv.texture.size.Height),
width: float32(tv.texture.size.Width),
height: float32(tv.texture.size.Height),
maxDepth: 1,
})

// Record MSAA resolve target if present.
Expand Down Expand Up @@ -849,25 +851,41 @@ type AttachDepthStencilCommand struct {

func (c *AttachDepthStencilCommand) Execute(ctx *gl.Context) {
if c.colorTexture.fbo == 0 {
return // No FBO was created; nothing to attach to.
return
}
attachment := depthStencilAttachmentPoint(c.depthTexture.format)
ctx.FramebufferTexture2D(gl.FRAMEBUFFER, attachment, c.depthTexture.target, c.depthTexture.id, 0)
}

// depthStencilAttachmentPoint returns the GL attachment point for a depth/stencil
// texture format. Matches Rust wgpu-hal GLES (command.rs:577-580).
func depthStencilAttachmentPoint(format gputypes.TextureFormat) uint32 {
switch format {
case gputypes.TextureFormatDepth24PlusStencil8, gputypes.TextureFormatDepth32FloatStencil8:
return gl.DEPTH_STENCIL_ATTACHMENT
case gputypes.TextureFormatStencil8:
return gl.STENCIL_ATTACHMENT
default:
return gl.DEPTH_ATTACHMENT
}
// Attach the depth/stencil texture. Using DEPTH_STENCIL_ATTACHMENT covers
// combined depth+stencil formats (e.g., Depth24PlusStencil8). For
// depth-only formats the driver silently ignores the stencil part.
// Use the texture's actual target (GL_TEXTURE_2D or GL_TEXTURE_2D_MULTISAMPLE).
ctx.FramebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, c.depthTexture.target, c.depthTexture.id, 0)
}

// AttachDepthStencilToFBOCommand attaches a depth/stencil texture to the
// currently bound FBO. Unlike AttachDepthStencilCommand, this does not
// reference a color texture — it operates on whatever FBO is currently bound
// (typically the surface swapchain FBO).
//
// The attachment point is chosen by texture format (Rust wgpu-hal command.rs:577-580):
// - depth-only → GL_DEPTH_ATTACHMENT
// - stencil-only → GL_STENCIL_ATTACHMENT
// - depth+stencil → GL_DEPTH_STENCIL_ATTACHMENT
type AttachDepthStencilToFBOCommand struct {
depthTexture *Texture
}

func (c *AttachDepthStencilToFBOCommand) Execute(ctx *gl.Context) {
ctx.FramebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, c.depthTexture.target, c.depthTexture.id, 0)
attachment := depthStencilAttachmentPoint(c.depthTexture.format)
ctx.FramebufferTexture2D(gl.FRAMEBUFFER, attachment, c.depthTexture.target, c.depthTexture.id, 0)
}

// MSAAResolveCommand resolves an MSAA framebuffer to a single-sample framebuffer
Expand Down Expand Up @@ -960,6 +978,8 @@ type ClearDepthCommand struct {

func (c *ClearDepthCommand) Execute(ctx *gl.Context) {
ctx.Disable(gl.SCISSOR_TEST)
ctx.DepthMask(true)
ctx.ClearDepth(c.depth)
ctx.Clear(gl.DEPTH_BUFFER_BIT)
}

Expand Down Expand Up @@ -1310,6 +1330,7 @@ type SetViewportCommand struct {

func (c *SetViewportCommand) Execute(ctx *gl.Context) {
ctx.Viewport(int32(c.x), int32(c.y), int32(c.width), int32(c.height))
ctx.DepthRange(float64(c.minDepth), float64(c.maxDepth))
}

// SetScissorCommand sets the scissor rectangle.
Expand Down
9 changes: 8 additions & 1 deletion hal/gles/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,14 @@ func (d *Device) UnmapBuffer(buffer hal.Buffer) error {
if buf.mapped == nil {
return nil
}
if buf.usage&gputypes.BufferUsageMapWrite != 0 && buf.id != 0 {
// Flush the CPU-side shadow buffer to the GL buffer. This must happen for
// ALL writable mappings, not only MapWrite buffers. MappedAtCreation works
// with any buffer usage (Uniform, Vertex, Index, CopyDst) per the WebGPU
// spec: "mappedAtCreation does not require MAP_WRITE usage." Without this
// flush, data written via MappedRange is silently discarded and the GL
// buffer remains zero-filled — uniform buffers get zero matrices, vertex
// buffers get zero positions, etc.
if buf.id != 0 {
glCtx := d.ctx.Lock()
glCtx.BindBuffer(buf.target, buf.id)
glCtx.BufferSubData(buf.target, 0, len(buf.mapped), unsafe.Pointer(&buf.mapped[0]))
Expand Down
9 changes: 8 additions & 1 deletion hal/gles/device_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,14 @@ func (d *Device) UnmapBuffer(buffer hal.Buffer) error {
if buf.mapped == nil {
return nil
}
if buf.usage&gputypes.BufferUsageMapWrite != 0 && buf.id != 0 {
// Flush the CPU-side shadow buffer to the GL buffer. This must happen for
// ALL writable mappings, not only MapWrite buffers. MappedAtCreation works
// with any buffer usage (Uniform, Vertex, Index, CopyDst) per the WebGPU
// spec: "mappedAtCreation does not require MAP_WRITE usage." Without this
// flush, data written via MappedRange is silently discarded and the GL
// buffer remains zero-filled — uniform buffers get zero matrices, vertex
// buffers get zero positions, etc.
if buf.id != 0 {
d.glCtx.BindBuffer(buf.target, buf.id)
d.glCtx.BufferSubData(buf.target, 0, len(buf.mapped), unsafe.Pointer(&buf.mapped[0]))
d.glCtx.BindBuffer(buf.target, 0)
Expand Down
15 changes: 15 additions & 0 deletions hal/gles/gl/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,21 @@ func (c *Context) ClearColor(r, g, b, a float32) {
uintptr(*(*uint32)(unsafe.Pointer(&a))))
}

// ClearDepth sets the depth value used by glClear(GL_DEPTH_BUFFER_BIT).
// Desktop GL uses glClearDepth (double); GLES uses glClearDepthf (float).
// Both variants are loaded via getProcAddr in init.
func (c *Context) ClearDepth(depth float64) {
syscall.SyscallN(c.glClearDepth, uintptr(*(*uint64)(unsafe.Pointer(&depth))))
}

// DepthRange sets the mapping of NDC depth to window depth.
// Desktop GL: glDepthRange(double, double). GLES: glDepthRangef(float, float).
func (c *Context) DepthRange(near, far float64) {
syscall.SyscallN(c.glDepthRange,
uintptr(*(*uint64)(unsafe.Pointer(&near))),
uintptr(*(*uint64)(unsafe.Pointer(&far))))
}

func (c *Context) Viewport(x, y, width, height int32) {
syscall.SyscallN(c.glViewport, uintptr(x), uintptr(y), uintptr(width), uintptr(height))
}
Expand Down
48 changes: 47 additions & 1 deletion hal/gles/gl/context_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ var (
cifVoid6TexMS types.CallInterface // void fn(uint32, int32, uint32, int32, int32, uint8) - TexImage2DMultisample
cifVoid10Blit types.CallInterface // void fn(int32*8, uint32, uint32) - BlitFramebuffer
cifVoid3UUF types.CallInterface // void fn(uint32, uint32, float32) - SamplerParameterf
cifVoid1Float types.CallInterface // void fn(float32) - ClearDepthf
cifVoid2Float types.CallInterface // void fn(float32, float32) - DepthRangef
cifInitialized bool
)

Expand Down Expand Up @@ -120,6 +122,25 @@ func initCommonCallInterfaces() error {
return err
}

// void fn(float32) - glClearDepthf
err = ffi.PrepareCallInterface(&cifVoid1Float, types.DefaultCall,
types.VoidTypeDescriptor,
[]*types.TypeDescriptor{types.FloatTypeDescriptor})
if err != nil {
return err
}

// void fn(float32, float32) - glDepthRangef
err = ffi.PrepareCallInterface(&cifVoid2Float, types.DefaultCall,
types.VoidTypeDescriptor,
[]*types.TypeDescriptor{
types.FloatTypeDescriptor,
types.FloatTypeDescriptor,
})
if err != nil {
return err
}

// void* fn(uint32)
err = ffi.PrepareCallInterface(&cifPtr1, types.DefaultCall,
types.PointerTypeDescriptor,
Expand Down Expand Up @@ -390,6 +411,8 @@ func initCommonCallInterfaces() error {
// Context holds OpenGL function pointers loaded at runtime via goffi.
// Functions are loaded via eglGetProcAddress for all OpenGL functions.
type Context struct {
isGLES bool

// Core GL 1.1
glGetError unsafe.Pointer
glGetString unsafe.Pointer
Expand Down Expand Up @@ -564,6 +587,7 @@ type ProcAddressFunc func(name string) unsafe.Pointer
// contexts that generate GL_INVALID_ENUM at call time.
func (c *Context) Load(getProcAddr ProcAddressFunc, isGLES ...bool) error {
gles := len(isGLES) > 0 && isGLES[0]
c.isGLES = gles
// Initialize common CallInterfaces
if err := initCommonCallInterfaces(); err != nil {
return err
Expand Down Expand Up @@ -692,7 +716,11 @@ func (c *Context) Load(getProcAddr ProcAddressFunc, isGLES ...bool) error {
// Depth/Stencil
c.glDepthFunc = getProcAddr("glDepthFunc")
c.glDepthMask = getProcAddr("glDepthMask")
c.glDepthRange = getProcAddr("glDepthRange")
if gles {
c.glDepthRange = getProcAddr("glDepthRangef")
} else {
c.glDepthRange = getProcAddr("glDepthRange")
}
c.glStencilFunc = getProcAddr("glStencilFunc")
c.glStencilOp = getProcAddr("glStencilOp")
c.glStencilMask = getProcAddr("glStencilMask")
Expand Down Expand Up @@ -824,6 +852,24 @@ func (c *Context) ClearColor(r, g, b, a float32) {
_, _ = ffi.CallFunction(&cifVoid4Float, c.glClearColor, nil, args[:])
}

// ClearDepth sets the depth value used by glClear(GL_DEPTH_BUFFER_BIT).
// On GLES the loaded function is glClearDepthf (float32).
// On desktop GL it is glClearDepth (double) — but Linux GLES contexts always
// load glClearDepthf, so we pass float32 unconditionally.
func (c *Context) ClearDepth(depth float64) {
d := float32(depth)
args := [1]unsafe.Pointer{unsafe.Pointer(&d)}
_, _ = ffi.CallFunction(&cifVoid1Float, c.glClearDepth, nil, args[:])
}

// DepthRange sets the mapping of NDC depth to window depth.
// On GLES the loaded function is glDepthRangef (float32).
func (c *Context) DepthRange(near, far float64) {
n, f := float32(near), float32(far)
args := [2]unsafe.Pointer{unsafe.Pointer(&n), unsafe.Pointer(&f)}
_, _ = ffi.CallFunction(&cifVoid2Float, c.glDepthRange, nil, args[:])
}

func (c *Context) Viewport(x, y, width, height int32) {
// Convert int32 to uint32 for API compatibility
ux, uy, uw, uh := uint32(x), uint32(y), uint32(width), uint32(height)
Expand Down
Loading