diff --git a/packages/rhi-webgl/src/GLRenderTarget.ts b/packages/rhi-webgl/src/GLRenderTarget.ts index 0ae8338ecb..d43665531a 100644 --- a/packages/rhi-webgl/src/GLRenderTarget.ts +++ b/packages/rhi-webgl/src/GLRenderTarget.ts @@ -11,22 +11,203 @@ import { import { GLTexture } from "./GLTexture"; import { WebGLGraphicDevice } from "./WebGLGraphicDevice"; +/** + * MSAA manager for WebGL render targets. + * Handles all MSAA-related operations separately from the main render target logic. + */ +class MSAAManager { + private _gl: WebGLRenderingContext & WebGL2RenderingContext; + private _isWebGL2: boolean; + private _target: RenderTarget; + private _frameBuffer: WebGLFramebuffer; + private _colorRenderBuffers: WebGLRenderbuffer[] = []; + private _depthRenderBuffer: WebGLRenderbuffer | null = null; + private _blitDrawBuffers: GLenum[] = []; + private _oriDrawBuffers: GLenum[]; + + constructor( + gl: WebGLRenderingContext & WebGL2RenderingContext, + isWebGL2: boolean, + target: RenderTarget, + oriDrawBuffers: GLenum[] + ) { + this._gl = gl; + this._isWebGL2 = isWebGL2; + this._target = target; + this._oriDrawBuffers = oriDrawBuffers; + this._frameBuffer = this._gl.createFramebuffer(); + + this._bindFBO(); + } + + /** + * Activate MSAA frame buffer for rendering. + */ + activate(): void { + this._gl.bindFramebuffer(this._gl.FRAMEBUFFER, this._frameBuffer); + } + + /** + * Resolve MSAA frame buffer to target frame buffer. + */ + resolveTo(targetFrameBuffer: WebGLFramebuffer): void { + const gl = this._gl; + const mask = gl.COLOR_BUFFER_BIT | (this._target.depthTexture ? gl.DEPTH_BUFFER_BIT : 0); + const { colorTextureCount, width, height } = this._target; + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this._frameBuffer); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, targetFrameBuffer); + + for (let textureIndex = 0; textureIndex < colorTextureCount; textureIndex++) { + const attachment = gl.COLOR_ATTACHMENT0 + textureIndex; + + this._blitDrawBuffers[textureIndex] = attachment; + + gl.readBuffer(attachment); + gl.drawBuffers(this._blitDrawBuffers); + gl.blitFramebuffer(0, 0, width, height, 0, 0, width, height, mask, gl.NEAREST); + + this._blitDrawBuffers[textureIndex] = gl.NONE; + } + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + } + + /** + * Destroy MSAA resources. + */ + destroy(): void { + const gl = this._gl; + + if (this._frameBuffer) { + gl.deleteFramebuffer(this._frameBuffer); + this._frameBuffer = null; + } + + if (this._depthRenderBuffer) { + gl.deleteRenderbuffer(this._depthRenderBuffer); + this._depthRenderBuffer = null; + } + + for (let i = 0; i < this._colorRenderBuffers.length; i++) { + gl.deleteRenderbuffer(this._colorRenderBuffers[i]); + } + this._colorRenderBuffers.length = 0; + } + + + + private _bindFBO(): void { + const gl = this._gl; + const isWebGL2 = this._isWebGL2; + + /** @ts-ignore */ + const { _depth, colorTextureCount } = this._target; + + this._blitDrawBuffers = new Array(colorTextureCount); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this._frameBuffer); + + // prepare MRT+MSAA color RBOs + for (let i = 0; i < colorTextureCount; i++) { + this._blitDrawBuffers[i] = gl.NONE; + + const internalFormat = /** @ts-ignore */ + (this._target.getColorTexture(i)._platformTexture as GLTexture)._formatDetail.internalFormat; + + this._colorRenderBuffers[i] = GLRenderTarget._createRenderBuffer(this._gl, this._target, internalFormat, gl.COLOR_ATTACHMENT0 + i); + } + gl.drawBuffers(this._oriDrawBuffers); + + // prepare MSAA depth RBO + if (_depth !== null) { + const { internalFormat, attachment } = + _depth instanceof Texture + ? /** @ts-ignore */ + (_depth._platformTexture as GLTexture)._formatDetail + : GLTexture._getRenderBufferDepthFormatDetail(_depth, gl, isWebGL2); + + this._depthRenderBuffer = GLRenderTarget._createRenderBuffer(this._gl, this._target, internalFormat, attachment); + } + + GLRenderTarget._checkFrameBufferStatus(gl); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + } +} + /** * The render target in WebGL platform is used for off-screen rendering. */ export class GLRenderTarget implements IPlatformRenderTarget { + /** + * @internal + */ + static _createRenderBuffer( + gl: WebGLRenderingContext & WebGL2RenderingContext, + target: RenderTarget, + internalFormat: GLenum, + attachment: GLenum + ): WebGLRenderbuffer { + const renderBuffer = gl.createRenderbuffer(); + const { width, height, antiAliasing } = target; + + gl.bindRenderbuffer(gl.RENDERBUFFER, renderBuffer); + + if (antiAliasing > 1) { + // Use MSAA storage + gl.renderbufferStorageMultisample(gl.RENDERBUFFER, antiAliasing, internalFormat, width, height); + } else { + // Use regular storage + gl.renderbufferStorage(gl.RENDERBUFFER, internalFormat, width, height); + } + + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachment, gl.RENDERBUFFER, renderBuffer); + + return renderBuffer; + } + + /** + * @internal + */ + static _checkFrameBufferStatus(gl: WebGLRenderingContext | WebGL2RenderingContext): void { + const e = gl.checkFramebufferStatus(gl.FRAMEBUFFER); + + switch (e) { + case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + throw new Error( + "The attachment types are mismatched or not all framebuffer attachment points are framebuffer attachment complete" + ); + case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + throw new Error("There is no attachment"); + case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: + throw new Error(" Height and width of the attachment are not the same."); + case gl.FRAMEBUFFER_UNSUPPORTED: + // #5.14.3 Event Types in https://registry.khronos.org/webgl/specs/1.0.0/ + if (!gl.isContextLost()) { + throw new Error( + "The format of the attachment is not supported or if depth and stencil attachments are not the same renderbuffer" + ); + } + break; + case (gl as WebGL2RenderingContext).FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: // Only for WebGL2 + throw new Error( + "The values of gl.RENDERBUFFER_SAMPLES are different among attached renderbuffers, or are non-zero if the attached images are a mix of renderbuffers and textures." + ); + } + } + private _gl: WebGLRenderingContext & WebGL2RenderingContext; private _isWebGL2: boolean; private _target: RenderTarget; private _frameBuffer: WebGLFramebuffer; - private _MSAAFrameBuffer: WebGLFramebuffer | null; private _depthRenderBuffer: WebGLRenderbuffer | null; - private _MSAAColorRenderBuffers: WebGLRenderbuffer[] = []; - private _MSAADepthRenderBuffer: WebGLRenderbuffer | null; private _oriDrawBuffers: GLenum[]; - private _blitDrawBuffers: GLenum[] | null; private _curMipLevel: number = 0; private _curFaceIndex: TextureCubeFace = undefined; + + // MSAA manager handles all MSAA-related operations + private _msaaManager: MSAAManager | null = null; /** * Create render target in WebGL platform. @@ -40,11 +221,6 @@ export class GLRenderTarget implements IPlatformRenderTarget { const { _colorTextures, _depth, width, height } = target; const isDepthTexture = _depth instanceof Texture; - /** todo - * MRT + Cube + [,MSAA] - * MRT + MSAA - */ - for (let i = 0, n = _colorTextures.length; i < n; i++) { const { format, isSRGBColorSpace } = _colorTextures[i]; if (!GLTexture._supportRenderBufferColorFormat(format, rhi)) { @@ -91,8 +267,7 @@ export class GLRenderTarget implements IPlatformRenderTarget { // bind MSAA FBO if (target.antiAliasing > 1) { - this._MSAAFrameBuffer = this._gl.createFramebuffer(); - this._bindMSAAFBO(); + this._msaaManager = new MSAAManager(this._gl, this._isWebGL2, target, this._oriDrawBuffers); } } @@ -151,8 +326,8 @@ export class GLRenderTarget implements IPlatformRenderTarget { this._curMipLevel = mipLevel; this._curFaceIndex = faceIndex; - if (this._MSAAFrameBuffer) { - gl.bindFramebuffer(gl.FRAMEBUFFER, this._MSAAFrameBuffer); + if (this._msaaManager) { + this._msaaManager.activate(); } } @@ -160,28 +335,9 @@ export class GLRenderTarget implements IPlatformRenderTarget { * Blit FBO. */ blitRenderTarget(): void { - if (!this._MSAAFrameBuffer) return; - - const gl = this._gl; - const mask = gl.COLOR_BUFFER_BIT | (this._target.depthTexture ? gl.DEPTH_BUFFER_BIT : 0); - const { colorTextureCount, width, height } = this._target; - - gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this._MSAAFrameBuffer); - gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this._frameBuffer); - - for (let textureIndex = 0; textureIndex < colorTextureCount; textureIndex++) { - const attachment = gl.COLOR_ATTACHMENT0 + textureIndex; - - this._blitDrawBuffers[textureIndex] = attachment; - - gl.readBuffer(attachment); - gl.drawBuffers(this._blitDrawBuffers); - gl.blitFramebuffer(0, 0, width, height, 0, 0, width, height, mask, gl.NEAREST); - - this._blitDrawBuffers[textureIndex] = gl.NONE; + if (this._msaaManager) { + this._msaaManager.resolveTo(this._frameBuffer); } - - gl.bindFramebuffer(gl.FRAMEBUFFER, null); } /** @@ -192,18 +348,14 @@ export class GLRenderTarget implements IPlatformRenderTarget { this._frameBuffer && gl.deleteFramebuffer(this._frameBuffer); this._depthRenderBuffer && gl.deleteRenderbuffer(this._depthRenderBuffer); - this._MSAAFrameBuffer && gl.deleteFramebuffer(this._MSAAFrameBuffer); - this._MSAADepthRenderBuffer && gl.deleteRenderbuffer(this._MSAADepthRenderBuffer); - for (let i = 0; i < this._MSAAColorRenderBuffers.length; i++) { - gl.deleteRenderbuffer(this._MSAAColorRenderBuffers[i]); + if (this._msaaManager) { + this._msaaManager.destroy(); + this._msaaManager = null; } this._frameBuffer = null; this._depthRenderBuffer = null; - this._MSAAFrameBuffer = null; - this._MSAAColorRenderBuffers.length = 0; - this._MSAADepthRenderBuffer = null; } private _bindMainFBO(): void { @@ -254,96 +406,11 @@ export class GLRenderTarget implements IPlatformRenderTarget { ); } else if (this._target.antiAliasing <= 1) { const { internalFormat, attachment } = GLTexture._getRenderBufferDepthFormatDetail(_depth, gl, isWebGL2); - const depthRenderBuffer = gl.createRenderbuffer(); - - this._depthRenderBuffer = depthRenderBuffer; - - gl.bindRenderbuffer(gl.RENDERBUFFER, depthRenderBuffer); - gl.renderbufferStorage(gl.RENDERBUFFER, internalFormat, width, height); - gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachment, gl.RENDERBUFFER, depthRenderBuffer); + this._depthRenderBuffer = GLRenderTarget._createRenderBuffer(gl, this._target, internalFormat, attachment); } } gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null); } - - private _bindMSAAFBO(): void { - const gl = this._gl; - const isWebGL2 = this._isWebGL2; - const MSAADepthRenderBuffer = gl.createRenderbuffer(); - - /** @ts-ignore */ - const { _depth, colorTextureCount, antiAliasing, width, height } = this._target; - - this._blitDrawBuffers = new Array(colorTextureCount); - this._MSAADepthRenderBuffer = MSAADepthRenderBuffer; - - gl.bindFramebuffer(gl.FRAMEBUFFER, this._MSAAFrameBuffer); - - // prepare MRT+MSAA color RBOs - for (let i = 0; i < colorTextureCount; i++) { - const MSAAColorRenderBuffer = gl.createRenderbuffer(); - - this._MSAAColorRenderBuffers[i] = MSAAColorRenderBuffer; - this._blitDrawBuffers[i] = gl.NONE; - - gl.bindRenderbuffer(gl.RENDERBUFFER, MSAAColorRenderBuffer); - gl.renderbufferStorageMultisample( - gl.RENDERBUFFER, - antiAliasing, - /** @ts-ignore */ - (this._target.getColorTexture(i)._platformTexture as GLTexture)._formatDetail.internalFormat, - width, - height - ); - gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.RENDERBUFFER, MSAAColorRenderBuffer); - } - gl.drawBuffers(this._oriDrawBuffers); - - // prepare MSAA depth RBO - if (_depth !== null) { - const { internalFormat, attachment } = - _depth instanceof Texture - ? /** @ts-ignore */ - (_depth._platformTexture as GLTexture)._formatDetail - : GLTexture._getRenderBufferDepthFormatDetail(_depth, gl, isWebGL2); - - gl.bindRenderbuffer(gl.RENDERBUFFER, MSAADepthRenderBuffer); - gl.renderbufferStorageMultisample(gl.RENDERBUFFER, antiAliasing, internalFormat, width, height); - gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachment, gl.RENDERBUFFER, MSAADepthRenderBuffer); - } - - this._checkFrameBuffer(); - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - gl.bindRenderbuffer(gl.RENDERBUFFER, null); - } - - private _checkFrameBuffer(): void { - const gl = this._gl; - const e = gl.checkFramebufferStatus(gl.FRAMEBUFFER); - - switch (e) { - case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: - throw new Error( - "The attachment types are mismatched or not all framebuffer attachment points are framebuffer attachment complete" - ); - case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: - throw new Error("There is no attachment"); - case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: - throw new Error(" Height and width of the attachment are not the same."); - case gl.FRAMEBUFFER_UNSUPPORTED: - // #5.14.3 Event Types in https://registry.khronos.org/webgl/specs/1.0.0/ - if (!gl.isContextLost()) { - throw new Error( - "The format of the attachment is not supported or if depth and stencil attachments are not the same renderbuffer" - ); - } - break; - case gl.FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: // Only for WebGL2 - throw new Error( - "The values of gl.RENDERBUFFER_SAMPLES are different among attached renderbuffers, or are non-zero if the attached images are a mix of renderbuffers and textures." - ); - } - } } diff --git a/tests/src/rhi-webgl/GLRenderTarget.test.ts b/tests/src/rhi-webgl/GLRenderTarget.test.ts new file mode 100644 index 0000000000..beb0d744f8 --- /dev/null +++ b/tests/src/rhi-webgl/GLRenderTarget.test.ts @@ -0,0 +1,224 @@ +import { RenderTarget, Texture2D, TextureFormat } from "@galacean/engine-core"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { GLRenderTarget } from "@galacean/engine-rhi-webgl/src/GLRenderTarget"; +import { describe, beforeAll, beforeEach, expect, it, vi, afterEach } from "vitest"; + +describe("GLRenderTarget", () => { + let engine: WebGLEngine; + let gl: WebGLRenderingContext | WebGL2RenderingContext; + + beforeAll(async () => { + const canvas = document.createElement("canvas"); + engine = await WebGLEngine.create({ canvas }); + // @ts-ignore + gl = engine._hardwareRenderer.gl; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("checkFrameBufferStatus static method", () => { + it("should not throw error for complete framebuffer", () => { + // Mock a complete framebuffer + vi.spyOn(gl, 'checkFramebufferStatus').mockReturnValue(gl.FRAMEBUFFER_COMPLETE); + + expect(() => { + GLRenderTarget._checkFrameBufferStatus(gl); + }).not.toThrow(); + }); + + it("should throw error for incomplete attachment", () => { + vi.spyOn(gl, 'checkFramebufferStatus').mockReturnValue(gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT); + + expect(() => { + GLRenderTarget._checkFrameBufferStatus(gl); + }).toThrow("The attachment types are mismatched"); + }); + + it("should throw error for missing attachment", () => { + vi.spyOn(gl, 'checkFramebufferStatus').mockReturnValue(gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT); + + expect(() => { + GLRenderTarget._checkFrameBufferStatus(gl); + }).toThrow("There is no attachment"); + }); + + it("should throw error for dimension mismatch", () => { + vi.spyOn(gl, 'checkFramebufferStatus').mockReturnValue(gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS); + + expect(() => { + GLRenderTarget._checkFrameBufferStatus(gl); + }).toThrow("Height and width of the attachment are not the same"); + }); + + it("should throw error for unsupported format when context is not lost", () => { + vi.spyOn(gl, 'checkFramebufferStatus').mockReturnValue(gl.FRAMEBUFFER_UNSUPPORTED); + vi.spyOn(gl, 'isContextLost').mockReturnValue(false); + + expect(() => { + GLRenderTarget._checkFrameBufferStatus(gl); + }).toThrow("The format of the attachment is not supported"); + }); + + it("should not throw error for unsupported format when context is lost", () => { + vi.spyOn(gl, 'checkFramebufferStatus').mockReturnValue(gl.FRAMEBUFFER_UNSUPPORTED); + vi.spyOn(gl, 'isContextLost').mockReturnValue(true); + + expect(() => { + GLRenderTarget._checkFrameBufferStatus(gl); + }).not.toThrow(); + }); + + it("should handle WebGL2 multisample error if available", () => { + // Only test on WebGL2 + if ('FRAMEBUFFER_INCOMPLETE_MULTISAMPLE' in gl) { + vi.spyOn(gl, 'checkFramebufferStatus').mockReturnValue( + (gl as WebGL2RenderingContext).FRAMEBUFFER_INCOMPLETE_MULTISAMPLE + ); + + expect(() => { + GLRenderTarget._checkFrameBufferStatus(gl); + }).toThrow("The values of gl.RENDERBUFFER_SAMPLES are different"); + } + }); + }); + + describe("MSAA functionality integration", () => { + it("should handle MSAA render targets without errors", () => { + // Mock checkFrameBufferStatus to avoid WebGL validation issues + const mockCheckFrameBuffer = vi.spyOn(GLRenderTarget, '_checkFrameBufferStatus') + .mockImplementation(() => {}); + + try { + const colorTexture = new Texture2D(engine, 512, 512); + + // Test creating MSAA render target + expect(() => { + const renderTarget = new RenderTarget(engine, 512, 512, colorTexture, TextureFormat.Depth16, 4); + renderTarget.destroy(); + }).not.toThrow(); + + } finally { + mockCheckFrameBuffer.mockRestore(); + } + }); + + it("should handle non-MSAA render targets correctly", () => { + expect(() => { + const colorTexture = new Texture2D(engine, 512, 512); + const renderTarget = new RenderTarget(engine, 512, 512, colorTexture, TextureFormat.Depth16, 1); + renderTarget.destroy(); + }).not.toThrow(); + }); + + it("should support basic render target operations", () => { + const mockCheckFrameBuffer = vi.spyOn(GLRenderTarget, '_checkFrameBufferStatus') + .mockImplementation(() => {}); + + try { + const colorTexture = new Texture2D(engine, 512, 512); + const renderTarget = new RenderTarget(engine, 512, 512, colorTexture, TextureFormat.Depth16, 4); + + // @ts-ignore - Access platform render target + const glRenderTarget = renderTarget._platformRenderTarget as GLRenderTarget; + + // Test that we can call activeRenderTarget without errors + expect(() => { + glRenderTarget.activeRenderTarget(0); + }).not.toThrow(); + + // Test that we can call blitRenderTarget without errors + expect(() => { + glRenderTarget.blitRenderTarget(); + }).not.toThrow(); + + renderTarget.destroy(); + } finally { + mockCheckFrameBuffer.mockRestore(); + } + }); + }); + + + + describe("Error handling and validation", () => { + it("should validate texture format support", () => { + const colorTexture = new Texture2D(engine, 512, 512); + + // Test unsupported texture format should throw error (expected behavior) + expect(() => { + new RenderTarget(engine, 512, 512, colorTexture, TextureFormat.R32G32B32A32, 1); + }).toThrow("this TextureFormat is not supported"); + }); + + it("should validate color texture size consistency", () => { + // Mock checkFrameBufferStatus to focus on size validation + const mockCheckFrameBuffer = vi.spyOn(GLRenderTarget, '_checkFrameBufferStatus') + .mockImplementation(() => {}); + + try { + const colorTexture1 = new Texture2D(engine, 512, 512); + const colorTexture2 = new Texture2D(engine, 256, 256); // Different size + + expect(() => { + new RenderTarget(engine, 512, 512, [colorTexture1, colorTexture2], TextureFormat.Depth16, 1); + }).toThrow("ColorTexture's size must as same as RenderTarget"); + } finally { + mockCheckFrameBuffer.mockRestore(); + } + }); + + it("should handle MSAA level auto-downgrade", () => { + // Mock maxAntiAliasing capability + // @ts-ignore + const originalMaxAA = engine._hardwareRenderer.capability.maxAntiAliasing; + // @ts-ignore + engine._hardwareRenderer.capability._maxAntiAliasing = 2; + + const mockCheckFrameBuffer = vi.spyOn(GLRenderTarget, '_checkFrameBufferStatus') + .mockImplementation(() => {}); + + try { + const colorTexture = new Texture2D(engine, 512, 512); + const renderTarget = new RenderTarget(engine, 512, 512, colorTexture, TextureFormat.Depth16, 8); + + // Should be downgraded to max supported level + expect(renderTarget.antiAliasing).toBe(2); + + renderTarget.destroy(); + } finally { + // Restore original maxAntiAliasing + // @ts-ignore + engine._hardwareRenderer.capability._maxAntiAliasing = originalMaxAA; + mockCheckFrameBuffer.mockRestore(); + } + }); + }); + + describe("Basic render target lifecycle", () => { + it("should create and destroy render targets without errors", () => { + expect(() => { + const colorTexture = new Texture2D(engine, 512, 512); + const renderTarget = new RenderTarget(engine, 512, 512, colorTexture, TextureFormat.Depth16, 1); + renderTarget.destroy(); + }).not.toThrow(); + }); + + it("should handle render target operations without errors", () => { + const colorTexture = new Texture2D(engine, 512, 512); + const renderTarget = new RenderTarget(engine, 512, 512, colorTexture, TextureFormat.Depth16, 1); + + // @ts-ignore + const glRenderTarget = renderTarget._platformRenderTarget as GLRenderTarget; + + // Test that basic operations don't throw errors + expect(() => { + glRenderTarget.activeRenderTarget(0); + glRenderTarget.blitRenderTarget(); + }).not.toThrow(); + + renderTarget.destroy(); + }); + }); +}); \ No newline at end of file