// Copyright 2016 Hajime Hoshi // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !playstation5 package opengl import ( "errors" "fmt" "image" "runtime" "sync" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl/gl" "github.com/hajimehoshi/ebiten/v2/internal/shaderir" "github.com/hajimehoshi/ebiten/v2/internal/shaderir/glsl" ) type blendFactor int type blendOperation int func convertBlendFactor(f graphicsdriver.BlendFactor) blendFactor { switch f { case graphicsdriver.BlendFactorZero: return gl.ZERO case graphicsdriver.BlendFactorOne: return gl.ONE case graphicsdriver.BlendFactorSourceColor: return gl.SRC_COLOR case graphicsdriver.BlendFactorOneMinusSourceColor: return gl.ONE_MINUS_SRC_COLOR case graphicsdriver.BlendFactorSourceAlpha: return gl.SRC_ALPHA case graphicsdriver.BlendFactorOneMinusSourceAlpha: return gl.ONE_MINUS_SRC_ALPHA case graphicsdriver.BlendFactorDestinationColor: return gl.DST_COLOR case graphicsdriver.BlendFactorOneMinusDestinationColor: return gl.ONE_MINUS_DST_COLOR case graphicsdriver.BlendFactorDestinationAlpha: return gl.DST_ALPHA case graphicsdriver.BlendFactorOneMinusDestinationAlpha: return gl.ONE_MINUS_DST_ALPHA case graphicsdriver.BlendFactorSourceAlphaSaturated: return gl.SRC_ALPHA_SATURATE default: panic(fmt.Sprintf("opengl: invalid blend factor %d", f)) } } func convertBlendOperation(o graphicsdriver.BlendOperation) blendOperation { switch o { case graphicsdriver.BlendOperationAdd: return gl.FUNC_ADD case graphicsdriver.BlendOperationSubtract: return gl.FUNC_SUBTRACT case graphicsdriver.BlendOperationReverseSubtract: return gl.FUNC_REVERSE_SUBTRACT case graphicsdriver.BlendOperationMin: return gl.MIN case graphicsdriver.BlendOperationMax: return gl.MAX default: panic(fmt.Sprintf("opengl: invalid blend operation %d", o)) } } type ( textureNative uint32 renderbufferNative uint32 framebufferNative uint32 shader uint32 program uint32 buffer uint32 ) type ( uniformLocation int32 ) const ( invalidFramebuffer = (1 << 32) - 1 invalidUniform = -1 ) type context struct { ctx gl.Context locationCache *locationCache screenFramebuffer framebufferNative // This might not be the default frame buffer '0' (e.g. iOS). lastFramebuffer framebufferNative lastTexture textureNative lastRenderbuffer renderbufferNative lastViewportWidth int lastViewportHeight int lastBlend graphicsdriver.Blend maxTextureSize int maxTextureSizeOnce sync.Once initOnce sync.Once } func (c *context) bindTexture(t textureNative) { if c.lastTexture == t { return } c.ctx.BindTexture(gl.TEXTURE_2D, uint32(t)) c.lastTexture = t } func (c *context) bindRenderbuffer(r renderbufferNative) { if c.lastRenderbuffer == r { return } c.ctx.BindRenderbuffer(gl.RENDERBUFFER, uint32(r)) c.lastRenderbuffer = r } func (c *context) bindFramebuffer(f framebufferNative) { if c.lastFramebuffer == f { return } c.ctx.BindFramebuffer(gl.FRAMEBUFFER, uint32(f)) c.lastFramebuffer = f } func (c *context) setViewport(f *framebuffer) { c.bindFramebuffer(f.native) if c.lastViewportWidth == f.viewportWidth && c.lastViewportHeight == f.viewportHeight { return } // On some environments, viewport size must be within the framebuffer size. // e.g. Edge (#71), Chrome on GPD Pocket (#420), macOS Mojave (#691). // Use the same size of the framebuffer here. c.ctx.Viewport(0, 0, int32(f.viewportWidth), int32(f.viewportHeight)) // glViewport must be called at least at every frame on iOS. // As the screen framebuffer is the last render target, next SetViewport should be // the first call at a frame. if f.native == c.screenFramebuffer { c.lastViewportWidth = 0 c.lastViewportHeight = 0 } else { c.lastViewportWidth = f.viewportWidth c.lastViewportHeight = f.viewportHeight } } func (c *context) newScreenFramebuffer(width, height int) *framebuffer { return &framebuffer{ native: c.screenFramebuffer, viewportWidth: width, viewportHeight: height, } } func (c *context) getMaxTextureSize() int { c.maxTextureSizeOnce.Do(func() { c.maxTextureSize = c.ctx.GetInteger(gl.MAX_TEXTURE_SIZE) }) return c.maxTextureSize } func (c *context) reset() error { var err1 error c.initOnce.Do(func() { // Load OpenGL functions after WGL is initialized especially for Windows (#2452). if err := c.ctx.LoadFunctions(); err != nil { err1 = err return } }) if err1 != nil { return err1 } c.locationCache = newLocationCache() c.lastTexture = 0 c.lastFramebuffer = invalidFramebuffer c.lastViewportWidth = 0 c.lastViewportHeight = 0 c.lastBlend = graphicsdriver.Blend{} c.ctx.Enable(gl.BLEND) c.ctx.Enable(gl.SCISSOR_TEST) c.blend(graphicsdriver.BlendSourceOver) c.screenFramebuffer = framebufferNative(c.ctx.GetInteger(gl.FRAMEBUFFER_BINDING)) // TODO: Need to update screenFramebufferWidth/Height? return nil } func (c *context) blend(blend graphicsdriver.Blend) { if c.lastBlend == blend { return } c.lastBlend = blend c.ctx.BlendFuncSeparate( uint32(convertBlendFactor(blend.BlendFactorSourceRGB)), uint32(convertBlendFactor(blend.BlendFactorDestinationRGB)), uint32(convertBlendFactor(blend.BlendFactorSourceAlpha)), uint32(convertBlendFactor(blend.BlendFactorDestinationAlpha)), ) c.ctx.BlendEquationSeparate( uint32(convertBlendOperation(blend.BlendOperationRGB)), uint32(convertBlendOperation(blend.BlendOperationAlpha)), ) } func (c *context) newTexture(width, height int) (textureNative, error) { t := c.ctx.CreateTexture() if t <= 0 { return 0, errors.New("opengl: creating texture failed") } c.bindTexture(textureNative(t)) c.ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) c.ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) c.ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) c.ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) c.ctx.PixelStorei(gl.UNPACK_ALIGNMENT, 4) // Firefox warns the usage of textures without specifying pixels (#629, #2077) // // Error: WebGL warning: drawElements: This operation requires zeroing texture data. This is slow. // // In Ebitengine, textures are filled with pixels later by the filter that ignores destination, so it is fine // to leave textures as uninitialized here. Rather, extra memory allocating for initialization should be // avoided. // // See also https://stackoverflow.com/questions/57734645. c.ctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(width), int32(height), gl.RGBA, gl.UNSIGNED_BYTE, nil) return textureNative(t), nil } func (c *context) framebufferPixels(buf []byte, f *framebuffer, region image.Rectangle) error { if got, want := len(buf), 4*region.Dx()*region.Dy(); got != want { return fmt.Errorf("opengl: len(buf) must be %d but was %d at framebufferPixels", got, want) } c.ctx.Flush() c.bindFramebuffer(f.native) x := int32(region.Min.X) y := int32(region.Min.Y) width := int32(region.Dx()) height := int32(region.Dy()) c.ctx.ReadPixels(buf, x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE) return nil } func (c *context) framebufferPixelsToBuffer(f *framebuffer, buffer buffer, width, height int) { c.ctx.Flush() c.bindFramebuffer(f.native) c.ctx.BindBuffer(gl.PIXEL_PACK_BUFFER, uint32(buffer)) c.ctx.ReadPixels(nil, 0, 0, int32(width), int32(height), gl.RGBA, gl.UNSIGNED_BYTE) c.ctx.BindBuffer(gl.PIXEL_PACK_BUFFER, 0) } func (c *context) deleteTexture(t textureNative) { if c.lastTexture == t { c.lastTexture = 0 } c.ctx.DeleteTexture(uint32(t)) } func (c *context) newRenderbuffer(width, height int) (renderbufferNative, error) { r := c.ctx.CreateRenderbuffer() if r <= 0 { return 0, errors.New("opengl: creating renderbuffer failed") } renderbuffer := renderbufferNative(r) c.bindRenderbuffer(renderbuffer) var stencilFormat uint32 if c.ctx.IsES() { // https://docs.gl/es2/glRenderbufferStorage // > Must be one of the following symbolic constants: GL_RGBA4, GL_RGB565, GL_RGB5_A1, // > GL_DEPTH_COMPONENT16, or GL_STENCIL_INDEX8. // // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/renderbufferStorage // > A GLenum specifying the internal format of the renderbuffer. Possible values: // > * gl.RGBA4: 4 red bits, 4 green bits, 4 blue bits 4 alpha bits. // > * gl.RGB565: 5 red bits, 6 green bits, 5 blue bits. // > * gl.RGB5_A1: 5 red bits, 5 green bits, 5 blue bits, 1 alpha bit. // > * gl.DEPTH_COMPONENT16: 16 depth bits. // > * gl.STENCIL_INDEX8: 8 stencil bits. // > * gl.DEPTH_STENCIL stencilFormat = gl.STENCIL_INDEX8 } else { // GL_STENCIL_INDEX8 might not be available with OpenGL 2.1. // https://www.khronos.org/opengl/wiki/Image_Format // > There are only 2 depth/stencil formats, each providing 8 stencil bits: GL_DEPTH24_STENCIL8 and GL_DEPTH32F_STENCIL8. // > [...] // > Stencil formats can only be used for Textures if OpenGL 4.4 or ARB_texture_stencil8 is available. stencilFormat = gl.DEPTH24_STENCIL8 } c.ctx.RenderbufferStorage(gl.RENDERBUFFER, stencilFormat, int32(width), int32(height)) return renderbuffer, nil } func (c *context) deleteRenderbuffer(r renderbufferNative) { if c.lastRenderbuffer == r { c.lastRenderbuffer = 0 } c.ctx.DeleteRenderbuffer(uint32(r)) } func (c *context) newFramebuffer(texture textureNative, width, height int) (*framebuffer, error) { f := c.ctx.CreateFramebuffer() if f <= 0 { return nil, fmt.Errorf("opengl: creating framebuffer failed: the returned value is not positive but %d", f) } c.bindFramebuffer(framebufferNative(f)) c.ctx.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, uint32(texture), 0) if shouldCheckFramebufferStatus() { if s := c.ctx.CheckFramebufferStatus(gl.FRAMEBUFFER); s != gl.FRAMEBUFFER_COMPLETE { if s != 0 { return nil, fmt.Errorf("opengl: creating framebuffer failed: %v", s) } if e := c.ctx.GetError(); e != gl.NO_ERROR { return nil, fmt.Errorf("opengl: creating framebuffer failed: (glGetError) %d", e) } return nil, fmt.Errorf("opengl: creating framebuffer failed: unknown error") } } return &framebuffer{ native: framebufferNative(f), viewportWidth: width, viewportHeight: height, }, nil } func (c *context) bindStencilBuffer(f framebufferNative, r renderbufferNative) error { c.bindFramebuffer(f) c.ctx.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, uint32(r)) if shouldCheckFramebufferStatus() { if s := c.ctx.CheckFramebufferStatus(gl.FRAMEBUFFER); s != gl.FRAMEBUFFER_COMPLETE { return fmt.Errorf("opengl: glFramebufferRenderbuffer failed: %d", s) } } return nil } func (c *context) deleteFramebuffer(f framebufferNative) { if f == c.screenFramebuffer { return } // If a framebuffer to be deleted is bound, a newly bound framebuffer // will be a default framebuffer. // https://www.khronos.org/opengles/sdk/docs/man/xhtml/glDeleteFramebuffers.xml if c.lastFramebuffer == f { c.lastFramebuffer = invalidFramebuffer c.lastViewportWidth = 0 c.lastViewportHeight = 0 } c.ctx.DeleteFramebuffer(uint32(f)) } func (c *context) newShader(shaderType uint32, source string) (shader, error) { s := c.ctx.CreateShader(shaderType) if s == 0 { return 0, fmt.Errorf("opengl: glCreateShader failed: shader type: %d", shaderType) } c.ctx.ShaderSource(s, source) c.ctx.CompileShader(s) return shader(s), nil } func (c *context) newProgram(shaders []shader, attributes []string) (program, error) { p := c.ctx.CreateProgram() if p == 0 { return 0, errors.New("opengl: glCreateProgram failed") } for _, shader := range shaders { c.ctx.AttachShader(p, uint32(shader)) } for i, name := range attributes { c.ctx.BindAttribLocation(p, uint32(i), name) } c.ctx.LinkProgram(p) return program(p), nil } func (c *context) deleteProgram(p program) { c.locationCache.deleteProgram(p) if !c.ctx.IsProgram(uint32(p)) { return } c.ctx.DeleteProgram(uint32(p)) } func (c *context) uniformInt(p program, location string, v int) bool { l := c.locationCache.GetUniformLocation(c, p, location) if l == invalidUniform { return false } c.ctx.Uniform1i(int32(l), int32(v)) return true } func (c *context) uniforms(p program, location string, v []uint32, typ shaderir.Type) bool { l := c.locationCache.GetUniformLocation(c, p, location) if l == invalidUniform { return false } base := typ.Main if base == shaderir.Array { base = typ.Sub[0].Main } switch base { case shaderir.Float: c.ctx.Uniform1fv(int32(l), uint32sToFloat32s(v)) case shaderir.Int: c.ctx.Uniform1iv(int32(l), uint32sToInt32s(v)) case shaderir.Vec2: c.ctx.Uniform2fv(int32(l), uint32sToFloat32s(v)) case shaderir.Vec3: c.ctx.Uniform3fv(int32(l), uint32sToFloat32s(v)) case shaderir.Vec4: c.ctx.Uniform4fv(int32(l), uint32sToFloat32s(v)) case shaderir.IVec2: c.ctx.Uniform2iv(int32(l), uint32sToInt32s(v)) case shaderir.IVec3: c.ctx.Uniform3iv(int32(l), uint32sToInt32s(v)) case shaderir.IVec4: c.ctx.Uniform4iv(int32(l), uint32sToInt32s(v)) case shaderir.Mat2: c.ctx.UniformMatrix2fv(int32(l), uint32sToFloat32s(v)) case shaderir.Mat3: c.ctx.UniformMatrix3fv(int32(l), uint32sToFloat32s(v)) case shaderir.Mat4: c.ctx.UniformMatrix4fv(int32(l), uint32sToFloat32s(v)) default: panic(fmt.Sprintf("opengl: unexpected type: %s", typ.String())) } return true } func (c *context) newArrayBuffer(size int) buffer { b := c.ctx.CreateBuffer() c.ctx.BindBuffer(gl.ARRAY_BUFFER, b) c.ctx.BufferInit(gl.ARRAY_BUFFER, size, gl.DYNAMIC_DRAW) return buffer(b) } func (c *context) newElementArrayBuffer(size int) buffer { b := c.ctx.CreateBuffer() c.ctx.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, b) c.ctx.BufferInit(gl.ELEMENT_ARRAY_BUFFER, size, gl.DYNAMIC_DRAW) return buffer(b) } func (c *context) glslVersion() glsl.GLSLVersion { if c.ctx.IsES() { return glsl.GLSLVersionES300 } return glsl.GLSLVersionDefault } func shouldCheckFramebufferStatus() bool { // CheckFramebufferStatus is slow and should be avoided especially in browsers. // See https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#avoid_blocking_api_calls_in_production // // TODO: Should this be avoided in all environments? return runtime.GOOS != "js" }