// Copyright 2018 The Ebiten Authors // // 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. package atlas import ( "fmt" "image" "math" "math/bits" "runtime" "sync" "github.com/hajimehoshi/ebiten/v2/internal/graphics" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/packing" "github.com/hajimehoshi/ebiten/v2/internal/restorable" "github.com/hajimehoshi/ebiten/v2/internal/shaderir" ) var ( minSourceSize = 0 minDestinationSize = 0 maxSize = 0 ) func min(a, b int) int { if a < b { return a } return b } func appendDeferred(f func()) { deferredM.Lock() defer deferredM.Unlock() deferred = append(deferred, f) } func flushDeferred() { deferredM.Lock() fs := deferred deferred = nil deferredM.Unlock() for _, f := range fs { f() } } // baseCountToPutOnSourceBackend represents the base time duration when the image can be put onto an atlas. // Actual time duration is increased in an exponential way for each usage as a rendering target. const baseCountToPutOnSourceBackend = 10 func putImagesOnSourceBackend() { // The counter usedAsDestinationCount is updated at most once per frame (#2676). imagesUsedAsDestination.forEach(func(i *Image) { // This counter is not updated when the backend is created in this frame. if !i.backendCreatedInThisFrame && i.usedAsDestinationCount < math.MaxInt { i.usedAsDestinationCount++ } i.backendCreatedInThisFrame = false }) imagesUsedAsDestination.clear() imagesToPutOnSourceBackend.forEach(func(i *Image) { if i.usedAsSourceCount < math.MaxInt { i.usedAsSourceCount++ } if i.usedAsSourceCount >= baseCountToPutOnSourceBackend*(1< maxSize || hp > maxSize { panic(fmt.Sprintf("atlas: the image being put on an atlas is too big: width: %d, height: %d", i.width, i.height)) } typ := restorable.ImageTypeRegular if i.imageType == ImageTypeVolatile { typ = restorable.ImageTypeVolatile } i.backend = &backend{ restorable: restorable.NewImage(wp, hp, typ), source: asSource && typ == restorable.ImageTypeRegular, } theBackends = append(theBackends, i.backend) return } // Check if an existing backend is available. loop: for _, b := range theBackends { if b.source != asSource { continue } for _, bb := range forbiddenBackends { if b == bb { continue loop } } if n, ok := b.tryAlloc(wp, hp); ok { i.backend = b i.node = n return } } var width, height int if asSource { width, height = minSourceSize, minSourceSize } else { width, height = minDestinationSize, minDestinationSize } for wp > width { if width == maxSize { panic(fmt.Sprintf("atlas: the image being put on an atlas is too big: width: %d, height: %d", i.width, i.height)) } width *= 2 } for hp > height { if height == maxSize { panic(fmt.Sprintf("atlas: the image being put on an atlas is too big: width: %d, height: %d", i.width, i.height)) } height *= 2 } typ := restorable.ImageTypeRegular if i.imageType == ImageTypeVolatile { typ = restorable.ImageTypeVolatile } b := &backend{ restorable: restorable.NewImage(width, height, typ), page: packing.NewPage(width, height, maxSize), source: asSource, } theBackends = append(theBackends, b) n := b.page.Alloc(wp, hp) if n == nil { panic("atlas: Alloc result must not be nil at allocate") } i.backend = b i.node = n } func (i *Image) DumpScreenshot(graphicsDriver graphicsdriver.Graphics, path string, blackbg bool) (string, error) { backendsM.Lock() defer backendsM.Unlock() if !inFrame { panic("atlas: DumpScreenshots must be called in between BeginFrame and EndFrame") } return i.backend.restorable.Dump(graphicsDriver, path, blackbg, image.Rect(0, 0, i.width, i.height)) } func EndFrame() error { backendsM.Lock() defer backendsM.Unlock() defer func() { inFrame = false }() if !inFrame { panic("atlas: inFrame must be true in EndFrame") } for _, b := range theBackends { b.sourceInThisFrame = false } return nil } func SwapBuffers(graphicsDriver graphicsdriver.Graphics) error { func() { backendsM.Lock() defer backendsM.Unlock() if inFrame { panic("atlas: inFrame must be false in SwapBuffer") } }() if err := restorable.SwapBuffers(graphicsDriver); err != nil { return err } return nil } func floorPowerOf2(x int) int { if x <= 0 { return 0 } return 1 << (bits.Len(uint(x)) - 1) } func BeginFrame(graphicsDriver graphicsdriver.Graphics) error { backendsM.Lock() defer backendsM.Unlock() if inFrame { panic("atlas: inFrame must be false in BeginFrame") } inFrame = true var err error initOnce.Do(func() { err = restorable.InitializeGraphicsDriverState(graphicsDriver) if err != nil { return } if len(theBackends) != 0 { panic("atlas: all the images must be not on an atlas before the game starts") } // min*Size and maxSize can already be set for testings. if minSourceSize == 0 { minSourceSize = 1024 } if minDestinationSize == 0 { minDestinationSize = 16 } if maxSize == 0 { maxSize = floorPowerOf2(restorable.MaxImageSize(graphicsDriver)) } }) if err != nil { return err } // Restore images first before other image manipulations (#2075). if err := restorable.RestoreIfNeeded(graphicsDriver); err != nil { return err } flushDeferred() putImagesOnSourceBackend() return nil } func DumpImages(graphicsDriver graphicsdriver.Graphics, dir string) (string, error) { backendsM.Lock() defer backendsM.Unlock() if !inFrame { panic("atlas: DumpImages must be called in between BeginFrame and EndFrame") } return restorable.DumpImages(graphicsDriver, dir) }