package graphics import ( "fmt" "github.com/go-gl/gl/v3.3-core/gl" ) const ( VirtualWidth int32 = 1920 VirtualHeight int32 = 1080 ) var ( vfbo uint32 vfboTexture uint32 vfboDepthStencil uint32 blitVAO uint32 blitVBO uint32 blitShaderProg uint32 nativeW, nativeH int32 ) // InitVirtualFramebuffer creates the FBO at virtual resolution and the blit shader. // Call after OpenGL context is created. func InitVirtualFramebuffer(displayW, displayH int32) error { nativeW = displayW nativeH = displayH // --- FBO setup --- gl.GenFramebuffers(1, &vfbo) gl.BindFramebuffer(gl.FRAMEBUFFER, vfbo) gl.GenTextures(1, &vfboTexture) gl.BindTexture(gl.TEXTURE_2D, vfboTexture) gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, VirtualWidth, VirtualHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, vfboTexture, 0) // Depth/stencil renderbuffer gl.GenRenderbuffers(1, &vfboDepthStencil) gl.BindRenderbuffer(gl.RENDERBUFFER, vfboDepthStencil) gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, VirtualWidth, VirtualHeight) gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, vfboDepthStencil) if status := gl.CheckFramebufferStatus(gl.FRAMEBUFFER); status != gl.FRAMEBUFFER_COMPLETE { return fmt.Errorf("virtual framebuffer incomplete: 0x%X", status) } gl.BindFramebuffer(gl.FRAMEBUFFER, 0) // --- Fullscreen quad --- vertices := []float32{ // pos (x,y) texcoord (u,v) -1, -1, 0, 0, 1, -1, 1, 0, -1, 1, 0, 1, 1, 1, 1, 1, } gl.GenVertexArrays(1, &blitVAO) gl.GenBuffers(1, &blitVBO) gl.BindVertexArray(blitVAO) gl.BindBuffer(gl.ARRAY_BUFFER, blitVBO) gl.BufferData(gl.ARRAY_BUFFER, len(vertices)*4, gl.Ptr(vertices), gl.STATIC_DRAW) gl.EnableVertexAttribArray(0) gl.VertexAttribPointerWithOffset(0, 2, gl.FLOAT, false, 4*4, 0) gl.EnableVertexAttribArray(1) gl.VertexAttribPointerWithOffset(1, 2, gl.FLOAT, false, 4*4, 2*4) gl.BindVertexArray(0) // --- Blit shader --- var err error blitShaderProg, err = compileBlitShader() if err != nil { return fmt.Errorf("blit shader compilation failed: %w", err) } return nil } // BeginVirtualRender binds the FBO so all subsequent draws go to the virtual 1080p buffer. func BeginVirtualRender() { gl.BindFramebuffer(gl.FRAMEBUFFER, vfbo) gl.Viewport(0, 0, VirtualWidth, VirtualHeight) } // BlitToScreen draws the virtual buffer to the actual screen at native resolution. func BlitToScreen() { gl.BindFramebuffer(gl.FRAMEBUFFER, 0) gl.Viewport(0, 0, nativeW, nativeH) gl.ClearColor(0, 0, 0, 1) gl.Clear(gl.COLOR_BUFFER_BIT) gl.UseProgram(blitShaderProg) gl.ActiveTexture(gl.TEXTURE0) gl.BindTexture(gl.TEXTURE_2D, vfboTexture) gl.BindVertexArray(blitVAO) gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) gl.BindVertexArray(0) } // DestroyVirtualFramebuffer cleans up all FBO resources. func DestroyVirtualFramebuffer() { if blitShaderProg != 0 { gl.DeleteProgram(blitShaderProg) } if blitVBO != 0 { gl.DeleteBuffers(1, &blitVBO) } if blitVAO != 0 { gl.DeleteVertexArrays(1, &blitVAO) } if vfboDepthStencil != 0 { gl.DeleteRenderbuffers(1, &vfboDepthStencil) } if vfboTexture != 0 { gl.DeleteTextures(1, &vfboTexture) } if vfbo != 0 { gl.DeleteFramebuffers(1, &vfbo) } } func compileBlitShader() (uint32, error) { vertSrc := `#version 330 core layout(location = 0) in vec2 aPos; layout(location = 1) in vec2 aTexCoord; out vec2 vTexCoord; void main() { vTexCoord = aTexCoord; gl_Position = vec4(aPos, 0.0, 1.0); }` + "\x00" fragSrc := `#version 330 core in vec2 vTexCoord; out vec4 FragColor; uniform sampler2D screenTexture; void main() { FragColor = texture(screenTexture, vTexCoord); }` + "\x00" vertShader, err := compileShader(vertSrc, gl.VERTEX_SHADER) if err != nil { return 0, err } fragShader, err := compileShader(fragSrc, gl.FRAGMENT_SHADER) if err != nil { return 0, err } prog := gl.CreateProgram() gl.AttachShader(prog, vertShader) gl.AttachShader(prog, fragShader) gl.LinkProgram(prog) var status int32 gl.GetProgramiv(prog, gl.LINK_STATUS, &status) if status == gl.FALSE { var logLen int32 gl.GetProgramiv(prog, gl.INFO_LOG_LENGTH, &logLen) logStr := make([]byte, logLen) gl.GetProgramInfoLog(prog, logLen, nil, &logStr[0]) return 0, fmt.Errorf("shader link error: %s", string(logStr)) } gl.DeleteShader(vertShader) gl.DeleteShader(fragShader) return prog, nil } func compileShader(src string, shaderType uint32) (uint32, error) { shader := gl.CreateShader(shaderType) csrc, free := gl.Strs(src) gl.ShaderSource(shader, 1, csrc, nil) free() gl.CompileShader(shader) var status int32 gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status) if status == gl.FALSE { var logLen int32 gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLen) logStr := make([]byte, logLen) gl.GetShaderInfoLog(shader, logLen, nil, &logStr[0]) return 0, fmt.Errorf("shader compile error: %s", string(logStr)) } return shader, nil }