gpu/internal/opengl,internal: move sRGB emulation to OpenGL driver

There is only one driver but several backends (EGL, WebGL).

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2021-05-15 15:25:02 +02:00
parent e90c99a66c
commit 21c319ace5
14 changed files with 104 additions and 102 deletions
+2 -28
View File
@@ -8,13 +8,11 @@ import (
"gioui.org/gpu" "gioui.org/gpu"
"gioui.org/internal/gl" "gioui.org/internal/gl"
"gioui.org/internal/srgb"
) )
type context struct { type context struct {
ctx js.Value ctx js.Value
cnv js.Value cnv js.Value
srgbFBO *srgb.FBO
} }
func newContext(w *window) (*context, error) { func newContext(w *window) (*context, error) {
@@ -43,19 +41,9 @@ func (c *context) API() gpu.API {
} }
func (c *context) Release() { func (c *context) Release() {
if c.srgbFBO != nil {
c.srgbFBO.Release()
c.srgbFBO = nil
}
} }
func (c *context) Present() error { func (c *context) Present() error {
if c.srgbFBO != nil {
c.srgbFBO.Blit()
}
if c.srgbFBO != nil {
c.srgbFBO.AfterPresent()
}
if c.ctx.Call("isContextLost").Bool() { if c.ctx.Call("isContextLost").Bool() {
return errors.New("context lost") return errors.New("context lost")
} }
@@ -67,20 +55,6 @@ func (c *context) Lock() {}
func (c *context) Unlock() {} func (c *context) Unlock() {}
func (c *context) MakeCurrent() error { func (c *context) MakeCurrent() error {
if c.srgbFBO == nil {
var err error
c.srgbFBO, err = srgb.New(gl.Context(c.ctx))
if err != nil {
c.Release()
c.srgbFBO = nil
return err
}
}
w, h := c.cnv.Get("width").Int(), c.cnv.Get("height").Int()
if err := c.srgbFBO.Refresh(w, h); err != nil {
c.Release()
return err
}
return nil return nil
} }
+1 -1
View File
@@ -280,7 +280,7 @@ func (g *compute) Frame() error {
Y: (viewport.Y + tileHeightPx - 1) / tileHeightPx, Y: (viewport.Y + tileHeightPx - 1) / tileHeightPx,
} }
defFBO := g.ctx.BeginFrame() defFBO := g.ctx.BeginFrame(viewport)
defer g.ctx.EndFrame() defer g.ctx.EndFrame()
if g.drawOps.profile && g.timers.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) { if g.drawOps.profile && g.timers.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
+2 -2
View File
@@ -425,9 +425,9 @@ func (g *gpu) Collect(viewport image.Point, frameOps *op.Ops) {
} }
func (g *gpu) Frame() error { func (g *gpu) Frame() error {
defFBO := g.ctx.BeginFrame()
defer g.ctx.EndFrame()
viewport := g.renderer.blitter.viewport viewport := g.renderer.blitter.viewport
defFBO := g.ctx.BeginFrame(viewport)
defer g.ctx.EndFrame()
for _, img := range g.drawOps.imageOps { for _, img := range g.drawOps.imageOps {
expandPathOp(img.path, img.clip) expandPathOp(img.path, img.clip)
} }
+1 -1
View File
@@ -174,7 +174,7 @@ func newDriver(t *testing.T) driver.Device {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
b.BeginFrame() b.BeginFrame(image.Pt(1, 1))
t.Cleanup(func() { t.Cleanup(func() {
b.EndFrame() b.EndFrame()
ctx.ReleaseCurrent() ctx.ReleaseCurrent()
+1 -1
View File
@@ -164,7 +164,7 @@ func newDirect3D11Device(api driver.Direct3D11) (driver.Device, error) {
return b, nil return b, nil
} }
func (b *Backend) BeginFrame() driver.Framebuffer { func (b *Backend) BeginFrame(viewport image.Point) driver.Framebuffer {
renderTarget, depthView := b.ctx.OMGetRenderTargets() renderTarget, depthView := b.ctx.OMGetRenderTargets()
// Assume someone else is holding on to the render targets. // Assume someone else is holding on to the render targets.
if renderTarget != nil { if renderTarget != nil {
+1 -1
View File
@@ -12,7 +12,7 @@ import (
// APIs such as OpenGL, Direct3D useful for rendering Gio // APIs such as OpenGL, Direct3D useful for rendering Gio
// operations. // operations.
type Device interface { type Device interface {
BeginFrame() Framebuffer BeginFrame(viewport image.Point) Framebuffer
EndFrame() EndFrame()
Caps() Caps Caps() Caps
NewTimer() Timer NewTimer() Timer
+39 -3
View File
@@ -12,6 +12,7 @@ import (
"gioui.org/gpu/internal/driver" "gioui.org/gpu/internal/driver"
"gioui.org/internal/gl" "gioui.org/internal/gl"
"gioui.org/internal/srgb"
) )
// Backend implements driver.Device. // Backend implements driver.Device.
@@ -30,6 +31,9 @@ type Backend struct {
// Single channel alpha textures. // Single channel alpha textures.
alphaTriple textureTriple alphaTriple textureTriple
srgbaTriple textureTriple srgbaTriple textureTriple
sRGBFBO *srgb.FBO
defFBO gl.Framebuffer
} }
// State tracking. // State tracking.
@@ -169,15 +173,43 @@ func newOpenGLDevice(api driver.OpenGL) (driver.Device, error) {
return b, nil return b, nil
} }
func (b *Backend) BeginFrame() driver.Framebuffer { func (b *Backend) BeginFrame(viewport image.Point) driver.Framebuffer {
// Assume GL state is reset between frames. // Assume GL state is reset between frames.
b.state = glstate{} b.state = glstate{}
fboID := gl.Framebuffer(b.funcs.GetBinding(gl.FRAMEBUFFER_BINDING)) b.defFBO = gl.Framebuffer(b.funcs.GetBinding(gl.FRAMEBUFFER_BINDING))
return &gpuFramebuffer{backend: b, obj: fboID, foreign: true} // Determine color encoding of the output framebuffer.
var fbEncoding int
if !b.defFBO.Valid() {
fbEncoding = b.funcs.GetFramebufferAttachmentParameteri(gl.FRAMEBUFFER, gl.BACK, gl.FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING)
} else {
fbEncoding = b.funcs.GetFramebufferAttachmentParameteri(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING)
}
renderFBO := b.defFBO
if fbEncoding == gl.LINEAR {
if b.sRGBFBO == nil {
// Framebuffer is not in sRGB format. Emulate it.
sfbo, err := srgb.New(b.funcs)
if err != nil {
panic(err)
}
b.sRGBFBO = sfbo
}
if err := b.sRGBFBO.Refresh(viewport); err != nil {
panic(err)
}
renderFBO = b.sRGBFBO.Framebuffer()
}
b.funcs.BindFramebuffer(gl.FRAMEBUFFER, renderFBO)
return &gpuFramebuffer{backend: b, obj: renderFBO, foreign: true}
} }
func (b *Backend) EndFrame() { func (b *Backend) EndFrame() {
b.funcs.ActiveTexture(gl.TEXTURE0) b.funcs.ActiveTexture(gl.TEXTURE0)
if b.sRGBFBO != nil {
b.funcs.BindFramebuffer(gl.FRAMEBUFFER, b.defFBO)
b.sRGBFBO.Blit()
}
b.funcs.BindFramebuffer(gl.FRAMEBUFFER, b.defFBO)
} }
func (b *Backend) Caps() driver.Caps { func (b *Backend) Caps() driver.Caps {
@@ -313,6 +345,10 @@ func glErr(f *gl.Functions) error {
} }
func (b *Backend) Release() { func (b *Backend) Release() {
if b.sRGBFBO != nil {
b.sRGBFBO.Release()
}
*b = Backend{}
} }
func (b *Backend) MemoryBarrier() { func (b *Backend) MemoryBarrier() {
+1 -32
View File
@@ -11,7 +11,6 @@ import (
"strings" "strings"
"gioui.org/gpu" "gioui.org/gpu"
"gioui.org/internal/srgb"
) )
type Context struct { type Context struct {
@@ -20,8 +19,6 @@ type Context struct {
eglSurf _EGLSurface eglSurf _EGLSurface
width, height int width, height int
refreshFBO bool refreshFBO bool
// For sRGB emulation.
srgbFBO *srgb.FBO
} }
type eglContext struct { type eglContext struct {
@@ -60,10 +57,6 @@ const (
) )
func (c *Context) Release() { func (c *Context) Release() {
if c.srgbFBO != nil {
c.srgbFBO.Release()
c.srgbFBO = nil
}
c.ReleaseSurface() c.ReleaseSurface()
if c.eglCtx != nil { if c.eglCtx != nil {
eglDestroyContext(c.disp, c.eglCtx.ctx) eglDestroyContext(c.disp, c.eglCtx.ctx)
@@ -73,15 +66,9 @@ func (c *Context) Release() {
} }
func (c *Context) Present() error { func (c *Context) Present() error {
if c.srgbFBO != nil {
c.srgbFBO.Blit()
}
if !eglSwapBuffers(c.disp, c.eglSurf) { if !eglSwapBuffers(c.disp, c.eglSurf) {
return fmt.Errorf("eglSwapBuffers failed (%x)", eglGetError()) return fmt.Errorf("eglSwapBuffers failed (%x)", eglGetError())
} }
if c.srgbFBO != nil {
c.srgbFBO.AfterPresent()
}
return nil return nil
} }
@@ -151,24 +138,6 @@ func (c *Context) MakeCurrent() error {
if !eglMakeCurrent(c.disp, c.eglSurf, c.eglSurf, c.eglCtx.ctx) { if !eglMakeCurrent(c.disp, c.eglSurf, c.eglSurf, c.eglCtx.ctx) {
return fmt.Errorf("eglMakeCurrent error 0x%x", eglGetError()) return fmt.Errorf("eglMakeCurrent error 0x%x", eglGetError())
} }
if c.eglCtx.srgb || c.eglSurf == nilEGLSurface {
return nil
}
if c.srgbFBO == nil {
var err error
c.srgbFBO, err = srgb.New(nil)
if err != nil {
c.ReleaseCurrent()
return err
}
}
if c.refreshFBO {
c.refreshFBO = false
if err := c.srgbFBO.Refresh(c.width, c.height); err != nil {
c.ReleaseCurrent()
return err
}
}
return nil return nil
} }
@@ -268,7 +237,7 @@ func createSurface(disp _EGLDisplay, eglCtx *eglContext, win NativeWindowType) (
surfAttribs = append(surfAttribs, _EGL_NONE) surfAttribs = append(surfAttribs, _EGL_NONE)
eglSurf := eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs) eglSurf := eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs)
if eglSurf == nilEGLSurface && eglCtx.srgb { if eglSurf == nilEGLSurface && eglCtx.srgb {
// Try again without sRGB // Try again without sRGB.
eglCtx.srgb = false eglCtx.srgb = false
surfAttribs = []_EGLint{_EGL_NONE} surfAttribs = []_EGLint{_EGL_NONE}
eglSurf = eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs) eglSurf = eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs)
+1
View File
@@ -10,6 +10,7 @@ type (
const ( const (
ALL_BARRIER_BITS = 0xffffffff ALL_BARRIER_BITS = 0xffffffff
ARRAY_BUFFER = 0x8892 ARRAY_BUFFER = 0x8892
BACK = 0x0405
BLEND = 0xbe2 BLEND = 0xbe2
CLAMP_TO_EDGE = 0x812f CLAMP_TO_EDGE = 0x812f
COLOR_ATTACHMENT0 = 0x8ce0 COLOR_ATTACHMENT0 = 0x8ce0
+9
View File
@@ -436,6 +436,15 @@ func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname
return int(f.ints[0]) return int(f.ints[0])
} }
func (f *Functions) GetInteger4(pname Enum) [4]int {
C.glGetIntegerv(C.GLenum(pname), &f.ints[0])
var r [4]int
for i := range r {
r[i] = int(f.ints[i])
}
return r
}
func (f *Functions) GetInteger(pname Enum) int { func (f *Functions) GetInteger(pname Enum) int {
C.glGetIntegerv(C.GLenum(pname), &f.ints[0]) C.glGetIntegerv(C.GLenum(pname), &f.ints[0])
return int(f.ints[0]) return int(f.ints[0])
+8
View File
@@ -292,6 +292,14 @@ func (c *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname
p, _, _ := syscall.Syscall(_glGetFramebufferAttachmentParameteri.Addr(), 3, uintptr(target), uintptr(attachment), uintptr(pname)) p, _, _ := syscall.Syscall(_glGetFramebufferAttachmentParameteri.Addr(), 3, uintptr(target), uintptr(attachment), uintptr(pname))
return int(p) return int(p)
} }
func (c *Functions) GetInteger4(pname Enum) [4]int {
syscall.Syscall(_glGetIntegerv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])), 0)
var r [4]int
for i := range r {
r[i] = int(c.int32s[i])
}
return r
}
func (c *Functions) GetInteger(pname Enum) int { func (c *Functions) GetInteger(pname Enum) int {
syscall.Syscall(_glGetIntegerv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])), 0) syscall.Syscall(_glGetIntegerv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])), 0)
return int(c.int32s[0]) return int(c.int32s[0])
+4
View File
@@ -14,6 +14,10 @@ type (
Object struct{ V uint } Object struct{ V uint }
) )
func (u Framebuffer) Valid() bool {
return u.V != 0
}
func (u Uniform) Valid() bool { func (u Uniform) Valid() bool {
return u.V != -1 return u.V != -1
} }
+4
View File
@@ -16,6 +16,10 @@ type (
Object js.Value Object js.Value
) )
func (f Framebuffer) Valid() bool {
return !js.Value(f).IsUndefined() && !js.Value(f).IsNull()
}
func (p Program) Valid() bool { func (p Program) Valid() bool {
return !js.Value(p).IsUndefined() && !js.Value(p).IsNull() return !js.Value(p).IsUndefined() && !js.Value(p).IsNull()
} }
+30 -33
View File
@@ -3,7 +3,9 @@
package srgb package srgb
import ( import (
"errors"
"fmt" "fmt"
"image"
"runtime" "runtime"
"strings" "strings"
@@ -15,22 +17,18 @@ import (
// for gamma-correct rendering on platforms without // for gamma-correct rendering on platforms without
// sRGB enabled native framebuffers. // sRGB enabled native framebuffers.
type FBO struct { type FBO struct {
c *gl.Functions c *gl.Functions
width, height int viewport image.Point
frameBuffer gl.Framebuffer srgbBuffer gl.Framebuffer
depthBuffer gl.Renderbuffer depthBuffer gl.Renderbuffer
colorTex gl.Texture colorTex gl.Texture
blitted bool blitted bool
quad gl.Buffer quad gl.Buffer
prog gl.Program prog gl.Program
gl3 bool gl3 bool
} }
func New(ctx gl.Context) (*FBO, error) { func New(f *gl.Functions) (*FBO, error) {
f, err := gl.NewFunctions(ctx)
if err != nil {
return nil, err
}
var gl3 bool var gl3 bool
glVer := f.GetString(gl.VERSION) glVer := f.GetString(gl.VERSION)
ver, _, err := gl.ParseGLVersion(glVer) ver, _, err := gl.ParseGLVersion(glVer)
@@ -48,7 +46,7 @@ func New(ctx gl.Context) (*FBO, error) {
s := &FBO{ s := &FBO{
c: f, c: f,
gl3: gl3, gl3: gl3,
frameBuffer: f.CreateFramebuffer(), srgbBuffer: f.CreateFramebuffer(),
colorTex: f.CreateTexture(), colorTex: f.CreateTexture(),
depthBuffer: f.CreateRenderbuffer(), depthBuffer: f.CreateRenderbuffer(),
} }
@@ -81,7 +79,6 @@ func (s *FBO) Blit() {
s.c.BufferSubData(gl.ARRAY_BUFFER, 0, coords) s.c.BufferSubData(gl.ARRAY_BUFFER, 0, coords)
s.blitted = true s.blitted = true
} }
s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{})
s.c.UseProgram(s.prog) s.c.UseProgram(s.prog)
s.c.BindTexture(gl.TEXTURE_2D, s.colorTex) s.c.BindTexture(gl.TEXTURE_2D, s.colorTex)
s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad) s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad)
@@ -93,38 +90,38 @@ func (s *FBO) Blit() {
s.c.BindTexture(gl.TEXTURE_2D, gl.Texture{}) s.c.BindTexture(gl.TEXTURE_2D, gl.Texture{})
s.c.DisableVertexAttribArray(0) s.c.DisableVertexAttribArray(0)
s.c.DisableVertexAttribArray(1) s.c.DisableVertexAttribArray(1)
s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer) s.c.BindFramebuffer(gl.FRAMEBUFFER, s.srgbBuffer)
s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0)
s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT) s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT)
// The Android emulator requires framebuffer 0 bound at eglSwapBuffer time.
// Bind the sRGB framebuffer again in afterPresent.
s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{})
} }
func (s *FBO) AfterPresent() { func (s *FBO) Framebuffer() gl.Framebuffer {
s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer) return s.srgbBuffer
} }
func (s *FBO) Refresh(w, h int) error { func (s *FBO) Refresh(viewport image.Point) error {
s.width, s.height = w, h if viewport.X == 0 || viewport.Y == 0 {
if w == 0 || h == 0 { return errors.New("srgb: zero-sized framebuffer")
}
if s.viewport == viewport {
return nil return nil
} }
s.viewport = viewport
s.c.BindTexture(gl.TEXTURE_2D, s.colorTex) s.c.BindTexture(gl.TEXTURE_2D, s.colorTex)
if s.gl3 { if s.gl3 {
s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB8_ALPHA8, w, h, gl.RGBA, gl.UNSIGNED_BYTE) s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB8_ALPHA8, viewport.X, viewport.Y, gl.RGBA, gl.UNSIGNED_BYTE)
} else /* EXT_sRGB */ { } else /* EXT_sRGB */ {
s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB_ALPHA_EXT, w, h, gl.SRGB_ALPHA_EXT, gl.UNSIGNED_BYTE) s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB_ALPHA_EXT, viewport.X, viewport.Y, gl.SRGB_ALPHA_EXT, gl.UNSIGNED_BYTE)
} }
currentRB := gl.Renderbuffer(s.c.GetBinding(gl.RENDERBUFFER_BINDING)) currentRB := gl.Renderbuffer(s.c.GetBinding(gl.RENDERBUFFER_BINDING))
s.c.BindRenderbuffer(gl.RENDERBUFFER, s.depthBuffer) s.c.BindRenderbuffer(gl.RENDERBUFFER, s.depthBuffer)
s.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h) s.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, viewport.X, viewport.Y)
s.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB) s.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB)
s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer) s.c.BindFramebuffer(gl.FRAMEBUFFER, s.srgbBuffer)
s.c.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, s.colorTex, 0) s.c.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, s.colorTex, 0)
s.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, s.depthBuffer) s.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, s.depthBuffer)
if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x error: %x", s.width, s.height, st, s.c.GetError()) return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x error: %x", viewport.X, viewport.Y, st, s.c.GetError())
} }
if runtime.GOOS == "js" { if runtime.GOOS == "js" {
@@ -136,9 +133,9 @@ func (s *FBO) Refresh(w, h int) error {
var pixel [4]byte var pixel [4]byte
s.c.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel[:]) s.c.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel[:])
if pixel[0] == 128 { // Correct sRGB color value is ~188 if pixel[0] == 128 { // Correct sRGB color value is ~188
s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, gl.RGBA, gl.UNSIGNED_BYTE) s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, viewport.X, viewport.Y, gl.RGBA, gl.UNSIGNED_BYTE)
if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
return fmt.Errorf("fallback RGBA framebuffer incomplete (%dx%d), status: %#x error: %x", s.width, s.height, st, s.c.GetError()) return fmt.Errorf("fallback RGBA framebuffer incomplete (%dx%d), status: %#x error: %x", viewport.X, viewport.Y, st, s.c.GetError())
} }
} }
} }
@@ -147,7 +144,7 @@ func (s *FBO) Refresh(w, h int) error {
} }
func (s *FBO) Release() { func (s *FBO) Release() {
s.c.DeleteFramebuffer(s.frameBuffer) s.c.DeleteFramebuffer(s.srgbBuffer)
s.c.DeleteTexture(s.colorTex) s.c.DeleteTexture(s.colorTex)
s.c.DeleteRenderbuffer(s.depthBuffer) s.c.DeleteRenderbuffer(s.depthBuffer)
if s.blitted { if s.blitted {