diff --git a/common/AnimatedEntity.go b/common/AnimatedEntity.go index 0468817ec..0d07f1174 100644 --- a/common/AnimatedEntity.go +++ b/common/AnimatedEntity.go @@ -2,6 +2,7 @@ package common import ( "fmt" + "image" "math" "strings" "time" @@ -107,7 +108,7 @@ func (v *AnimatedEntity) cacheFrames() { } direction := v.dcc.Directions[v.direction] frame := direction.Frames[frameIndex] - pixelData := make([]byte, 4*frameW*frameH) + img := image.NewRGBA(image.Rect(0, 0, int(frameW), int(frameH))) for y := 0; y < direction.Box.Height; y++ { for x := 0; x < direction.Box.Width; x++ { paletteIndex := frame.PixelData[x+(y*direction.Box.Width)] @@ -118,13 +119,15 @@ func (v *AnimatedEntity) cacheFrames() { color := Palettes[v.palette].Colors[paletteIndex] actualX := x + direction.Box.Left - int(minX) actualY := y + direction.Box.Top - int(minY) - pixelData[(actualX*4)+(actualY*int(frameW)*4)] = color.R - pixelData[(actualX*4)+(actualY*int(frameW)*4)+1] = color.G - pixelData[(actualX*4)+(actualY*int(frameW)*4)+2] = color.B - pixelData[(actualX*4)+(actualY*int(frameW)*4)+3] = 255 + img.Pix[(actualX*4)+(actualY*int(frameW)*4)] = color.R + img.Pix[(actualX*4)+(actualY*int(frameW)*4)+1] = color.G + img.Pix[(actualX*4)+(actualY*int(frameW)*4)+2] = color.B + img.Pix[(actualX*4)+(actualY*int(frameW)*4)+3] = 255 } } - v.frames[frameIndex].ReplacePixels(pixelData) + newImage, _ := ebiten.NewImageFromImage(img, ebiten.FilterNearest) + img = nil + v.frames[frameIndex] = newImage v.frameLocations[frameIndex] = Rectangle{ Left: int(minX), Top: int(minY), diff --git a/common/ColorConvert.go b/common/ColorConvert.go index d9a5bb39c..d5fe11f8d 100644 --- a/common/ColorConvert.go +++ b/common/ColorConvert.go @@ -3,7 +3,6 @@ package common import ( "image/color" "math" - "sync" "github.com/hajimehoshi/ebiten" ) @@ -16,7 +15,6 @@ type colorMCacheEntry struct { } var ( - textM sync.Mutex colorMCache = map[colorMCacheKey]*colorMCacheEntry{} emptyColorM ebiten.ColorM monotonicClock int64 diff --git a/common/Dcc.go b/common/Dcc.go index f67cabfb2..ee9b0cdbc 100644 --- a/common/Dcc.go +++ b/common/Dcc.go @@ -2,6 +2,7 @@ package common import ( "log" + "sync" ) type DCCPixelBufferEntry struct { @@ -337,10 +338,12 @@ func (v *DCCDirection) GenerateFrames(pcd *BitMuncher) { } v.Cells = nil v.PixelData = nil + v.PixelBuffer = nil } func (v *DCCDirection) FillPixelBuffer(pcd, ec, pm, et, rp *BitMuncher) { lastPixel := uint32(0) + pixelStack := make([]uint32, 4) maxCellX := 0 maxCellY := 0 for _, frame := range v.Frames { @@ -389,7 +392,6 @@ func (v *DCCDirection) FillPixelBuffer(pcd, ec, pm, et, rp *BitMuncher) { continue } // Decode the pixels - pixelStack := make([]uint32, 4) lastPixel = 0 numberOfPixelBits := pixelMaskLookup[pixelMask] encodingType := 0 @@ -512,9 +514,15 @@ func LoadDCC(path string, fileProvider FileProvider) *DCC { directionOffsets[i] = int(bm.GetInt32()) } result.Directions = make([]*DCCDirection, result.NumberOfDirections) + var wg sync.WaitGroup + wg.Add(result.NumberOfDirections) for i := 0; i < result.NumberOfDirections; i++ { - result.Directions[i] = CreateDCCDirection(CreateBitMuncher(fileData, directionOffsets[i]*8), result) + go func(i int) { + defer wg.Done() + result.Directions[i] = CreateDCCDirection(CreateBitMuncher(fileData, directionOffsets[i]*8), result) + }(i) } + wg.Wait() return result } diff --git a/common/Sprite.go b/common/Sprite.go index dcccca187..b7ee29172 100644 --- a/common/Sprite.go +++ b/common/Sprite.go @@ -2,7 +2,9 @@ package common import ( "encoding/binary" + "image" "image/color" + "sync" "time" "github.com/hajimehoshi/ebiten" @@ -36,7 +38,6 @@ type SpriteFrame struct { Length uint32 ImageData []int16 Image *ebiten.Image - Loaded bool } // CreateSprite creates an instance of a sprite @@ -63,33 +64,36 @@ func CreateSprite(data []byte, palette PaletteRec) *Sprite { dataPointer += 4 } result.Frames = make([]*SpriteFrame, totalFrames) + var wg sync.WaitGroup + wg.Add(int(totalFrames)) for i := uint32(0); i < totalFrames; i++ { - dataPointer = framePointers[i] - result.Frames[i] = &SpriteFrame{Loaded: false} - result.Frames[i].Flip = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) - dataPointer += 4 - result.Frames[i].Width = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) - dataPointer += 4 - result.Frames[i].Height = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) - dataPointer += 4 - result.Frames[i].OffsetX = BytesToInt32(data[dataPointer : dataPointer+4]) - dataPointer += 4 - result.Frames[i].OffsetY = BytesToInt32(data[dataPointer : dataPointer+4]) - dataPointer += 4 - result.Frames[i].Unknown = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) - dataPointer += 4 - result.Frames[i].NextBlock = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) - dataPointer += 4 - result.Frames[i].Length = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) - dataPointer += 4 - result.Frames[i].ImageData = make([]int16, result.Frames[i].Width*result.Frames[i].Height) - for fi := range result.Frames[i].ImageData { - result.Frames[i].ImageData[fi] = -1 - } + go func(i uint32) { + defer wg.Done() + dataPointer := framePointers[i] + result.Frames[i] = &SpriteFrame{} + result.Frames[i].Flip = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].Width = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].Height = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].OffsetX = BytesToInt32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].OffsetY = BytesToInt32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].Unknown = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].NextBlock = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].Length = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].ImageData = make([]int16, result.Frames[i].Width*result.Frames[i].Height) + for fi := range result.Frames[i].ImageData { + result.Frames[i].ImageData[fi] = -1 + } - x := uint32(0) - y := uint32(result.Frames[i].Height - 1) - go func(ix, dataPointer uint32) { + x := uint32(0) + y := uint32(result.Frames[i].Height - 1) for true { b := data[dataPointer] dataPointer++ @@ -102,41 +106,39 @@ func CreateSprite(data []byte, palette PaletteRec) *Sprite { } else if (b & 0x80) > 0 { transparentPixels := b & 0x7F for ti := byte(0); ti < transparentPixels; ti++ { - result.Frames[ix].ImageData[x+(y*result.Frames[ix].Width)+uint32(ti)] = -1 + result.Frames[i].ImageData[x+(y*result.Frames[i].Width)+uint32(ti)] = -1 } x += uint32(transparentPixels) } else { for bi := 0; bi < int(b); bi++ { - result.Frames[ix].ImageData[x+(y*result.Frames[ix].Width)+uint32(bi)] = int16(data[dataPointer]) + result.Frames[i].ImageData[x+(y*result.Frames[i].Width)+uint32(bi)] = int16(data[dataPointer]) dataPointer++ } x += uint32(b) } } - result.Frames[ix].Image, _ = ebiten.NewImage(int(result.Frames[ix].Width), int(result.Frames[ix].Height), ebiten.FilterNearest) - newData := make([]byte, result.Frames[ix].Width*result.Frames[ix].Height*4) - for ii := uint32(0); ii < result.Frames[ix].Width*result.Frames[ix].Height; ii++ { - if result.Frames[ix].ImageData[ii] < 1 { // TODO: Is this == -1 or < 1? + var img = image.NewRGBA(image.Rect(0, 0, int(result.Frames[i].Width), int(result.Frames[i].Height))) + for ii := uint32(0); ii < result.Frames[i].Width*result.Frames[i].Height; ii++ { + if result.Frames[i].ImageData[ii] < 1 { // TODO: Is this == -1 or < 1? continue } - newData[ii*4] = palette.Colors[result.Frames[ix].ImageData[ii]].R - newData[(ii*4)+1] = palette.Colors[result.Frames[ix].ImageData[ii]].G - newData[(ii*4)+2] = palette.Colors[result.Frames[ix].ImageData[ii]].B - newData[(ii*4)+3] = 0xFF + img.Pix[ii*4] = palette.Colors[result.Frames[i].ImageData[ii]].R + img.Pix[(ii*4)+1] = palette.Colors[result.Frames[i].ImageData[ii]].G + img.Pix[(ii*4)+2] = palette.Colors[result.Frames[i].ImageData[ii]].B + img.Pix[(ii*4)+3] = 0xFF } - result.Frames[ix].Image.ReplacePixels(newData) - result.Frames[ix].Loaded = true - }(i, dataPointer) + newImage, _ := ebiten.NewImageFromImage(img, ebiten.FilterNearest) + result.Frames[i].Image = newImage + img = nil + }(i) } + wg.Wait() return result } // GetSize returns the size of the sprite func (v *Sprite) GetSize() (uint32, uint32) { frame := v.Frames[uint32(v.Frame)+(uint32(v.Direction)*v.FramesPerDirection)] - for frame.Loaded == false { - time.Sleep(time.Millisecond * 5) - } return frame.Width, frame.Height } @@ -175,9 +177,6 @@ func (v *Sprite) OnLastFrame() bool { // GetFrameSize returns the size of the specific frame func (v *Sprite) GetFrameSize(frame int) (width, height uint32) { - for v.Frames[frame].Loaded == false { - time.Sleep(time.Millisecond) - } width = v.Frames[frame].Width height = v.Frames[frame].Height return @@ -193,9 +192,6 @@ func (v *Sprite) Draw(target *ebiten.Image) { v.updateAnimation() opts := &ebiten.DrawImageOptions{} frame := v.Frames[uint32(v.Frame)+(uint32(v.Direction)*v.FramesPerDirection)] - for frame.Loaded == false { - time.Sleep(time.Millisecond) - } opts.GeoM.Translate( float64(int32(v.X)+frame.OffsetX), float64((int32(v.Y) - int32(frame.Height) + frame.OffsetY)), @@ -233,9 +229,6 @@ func (v *Sprite) DrawSegments(target *ebiten.Image, xSegments, ySegments, offset if v.ColorMod != nil { opts.ColorM = ColorToColorM(v.ColorMod) } - for frame.Loaded == false { - time.Sleep(time.Millisecond * 5) - } target.DrawImage(frame.Image, opts) xOffset += int32(frame.Width) biggestYOffset = MaxInt32(biggestYOffset, int32(frame.Height)) diff --git a/common/Weapons.go b/common/Weapons.go index e4b6d7a35..364c9b153 100644 --- a/common/Weapons.go +++ b/common/Weapons.go @@ -255,7 +255,7 @@ func createWeaponVendorParams(r *[]string, inc func() int) map[string]*ItemVendo func CreateItemVendorParams(r *[]string, inc func() int, vs []string) map[string]*ItemVendorParams { result := make(map[string]*ItemVendorParams) - + for _, name := range vs { wvp := ItemVendorParams{ Min: StringToInt(EmptyToZero((*r)[inc()])), diff --git a/core/Engine.go b/core/Engine.go index a3b9d8df0..cdd5e411b 100644 --- a/core/Engine.go +++ b/core/Engine.go @@ -28,6 +28,8 @@ type Engine struct { CheckedPatch map[string]bool // First time we check a file, we'll check if it's in the patch. This notes that we've already checked that. LoadingSprite *common.Sprite // The sprite shown when loading stuff loadingProgress float64 // LoadingProcess is a range between 0.0 and 1.0. If set, loading screen displays. + loadingIndex int // Determines which load function is currently being called + thingsToLoad []func() // The load functions for the next scene stepLoadingSize float64 // The size for each loading step CurrentScene scenes.Scene // The current scene being rendered UIManager *ui.Manager // The UI manager @@ -129,6 +131,19 @@ func (v *Engine) LoadSprite(fileName string, palette palettedefs.PaletteType) *c // updateScene handles the scene maintenance for the engine func (v *Engine) updateScene() { if v.nextScene == nil { + if v.thingsToLoad != nil { + if v.loadingIndex < len(v.thingsToLoad) { + v.thingsToLoad[v.loadingIndex]() + v.loadingIndex++ + if v.loadingIndex < len(v.thingsToLoad) { + v.StepLoading() + } else { + v.FinishLoading() + v.thingsToLoad = nil + } + return + } + } return } if v.CurrentScene != nil { @@ -137,16 +152,10 @@ func (v *Engine) updateScene() { v.CurrentScene = v.nextScene v.nextScene = nil v.UIManager.Reset() - thingsToLoad := v.CurrentScene.Load() - v.SetLoadingStepSize(1.0 / float64(len(thingsToLoad))) + v.thingsToLoad = v.CurrentScene.Load() + v.loadingIndex = 0 + v.SetLoadingStepSize(1.0 / float64(len(v.thingsToLoad))) v.ResetLoading() - go func() { - for _, f := range thingsToLoad { - f() - v.StepLoading() - } - v.FinishLoading() - }() } // Update updates the internal state of the engine diff --git a/go.sum b/go.sum index cc7c71c5f..fa71cf1d8 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/profile v1.3.0 h1:OQIvuDgm00gWVWGTf4m4mCt6W1/0YqU7Ntg0mySWgaI= github.com/pkg/profile v1.3.0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/mpq/MPQ.go b/mpq/MPQ.go index c07794cd5..370688fe5 100644 --- a/mpq/MPQ.go +++ b/mpq/MPQ.go @@ -8,6 +8,7 @@ import ( "os" "path" "strings" + "sync" "github.com/OpenDiablo2/OpenDiablo2/resourcepaths" ) @@ -19,6 +20,7 @@ type MPQ struct { HashTableEntries []HashTableEntry BlockTableEntries []BlockTableEntry Data Data + fileCache map[string][]byte } // Data Represents a MPQ file @@ -92,21 +94,31 @@ func (v BlockTableEntry) HasFlag(flag FileFlag) bool { return (v.Flags & flag) != 0 } +var mpqMutex = sync.Mutex{} +var mpqCache = make(map[string]*MPQ) + // Load loads an MPQ file and returns a MPQ structure -func Load(fileName string) (MPQ, error) { - result := MPQ{ - FileName: fileName, +func Load(fileName string) (*MPQ, error) { + mpqMutex.Lock() + defer mpqMutex.Unlock() + cached := mpqCache[fileName] + if cached != nil { + return cached, nil + } + result := &MPQ{ + FileName: fileName, + fileCache: make(map[string][]byte), } file, err := os.Open(fileName) if err != nil { - return MPQ{}, err + return nil, err } result.File = file err = result.readHeader() if err != nil { - return MPQ{}, err + return nil, err } - + mpqCache[fileName] = result return result, nil } @@ -225,7 +237,7 @@ func (v MPQ) getFileHashEntry(fileName string) (HashTableEntry, error) { } // GetFileBlockData gets a block table entry -func (v MPQ) GetFileBlockData(fileName string) (BlockTableEntry, error) { +func (v MPQ) getFileBlockData(fileName string) (BlockTableEntry, error) { fileName = strings.ReplaceAll(fileName, "{LANG}", resourcepaths.LanguageCode) fileEntry, err := v.getFileHashEntry(fileName) if err != nil || fileEntry.BlockIndex >= uint32(len(v.BlockTableEntries)) { @@ -249,8 +261,12 @@ func (v MPQ) FileExists(fileName string) bool { // ReadFile reads a file from the MPQ and returns a memory stream func (v MPQ) ReadFile(fileName string) ([]byte, error) { + cached := v.fileCache[fileName] + if cached != nil { + return cached, nil + } fileName = strings.ReplaceAll(fileName, "{LANG}", resourcepaths.LanguageCode) - fileBlockData, err := v.GetFileBlockData(fileName) + fileBlockData, err := v.getFileBlockData(fileName) if err != nil { return []byte{}, err } @@ -259,6 +275,7 @@ func (v MPQ) ReadFile(fileName string) ([]byte, error) { mpqStream := CreateStream(v, fileBlockData, fileName) buffer := make([]byte, fileBlockData.UncompressedFileSize) mpqStream.Read(buffer, 0, fileBlockData.UncompressedFileSize) + v.fileCache[fileName] = buffer return buffer, nil } diff --git a/tests/MapLoad_test.go b/tests/MapLoad_test.go index 916699f04..afe4ff1e7 100644 --- a/tests/MapLoad_test.go +++ b/tests/MapLoad_test.go @@ -15,6 +15,7 @@ import ( func TestMapGenerationPerformance(t *testing.T) { mpq.InitializeCryptoBuffer() common.ConfigBasePath = "../" + engine := core.CreateEngine() gameState := common.CreateGameState() mapEngine := _map.CreateMapEngine(gameState, engine.SoundManager, engine)