diff --git a/CHANGELOG.md b/CHANGELOG.md index c623d1f2..72707fcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/hal/gles/command.go b/hal/gles/command.go index 42abf1b0..2c025357 100644 --- a/hal/gles/command.go +++ b/hal/gles/command.go @@ -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. @@ -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. @@ -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 @@ -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) } @@ -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. diff --git a/hal/gles/device.go b/hal/gles/device.go index a6834c2b..a2bb2d5d 100644 --- a/hal/gles/device.go +++ b/hal/gles/device.go @@ -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])) diff --git a/hal/gles/device_linux.go b/hal/gles/device_linux.go index e7f28a9f..72af0c30 100644 --- a/hal/gles/device_linux.go +++ b/hal/gles/device_linux.go @@ -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) diff --git a/hal/gles/gl/context.go b/hal/gles/gl/context.go index 5fc4738a..2c5cabe5 100644 --- a/hal/gles/gl/context.go +++ b/hal/gles/gl/context.go @@ -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)) } diff --git a/hal/gles/gl/context_linux.go b/hal/gles/gl/context_linux.go index 2e9e20fc..577a81ee 100644 --- a/hal/gles/gl/context_linux.go +++ b/hal/gles/gl/context_linux.go @@ -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 ) @@ -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, @@ -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 @@ -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 @@ -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") @@ -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)