From 90fe89a92589b0e96db7afb3cc03e34fb0f4e1a3 Mon Sep 17 00:00:00 2001 From: tobigun Date: Wed, 13 Aug 2008 12:14:21 +0000 Subject: - new cover-loading. Cover thumbnails are stored in an sqlite db (cover.db) now instead of the proprietary covers.cache. - covers are cached faster (-> start-up on first run will be considerably faster now if many covers exist). git-svn-id: svn://svn.code.sf.net/p/ultrastardx/svn/trunk@1256 b956fd51-792f-4845-bead-9b4dfca2ff2c --- Game/Code/Classes/UCovers.pas | 548 +++++++++++++++++++++++++------------- Game/Code/Classes/UIni.pas | 5 +- Game/Code/Classes/UMain.pas | 2 +- Game/Code/Classes/USongs.pas | 4 +- Game/Code/Screens/UScreenSong.pas | 96 +++---- 5 files changed, 405 insertions(+), 250 deletions(-) (limited to 'Game') diff --git a/Game/Code/Classes/UCovers.pas b/Game/Code/Classes/UCovers.pas index 1ff2a5c0..7bb57b4a 100644 --- a/Game/Code/Classes/UCovers.pas +++ b/Game/Code/Classes/UCovers.pas @@ -1,5 +1,15 @@ unit UCovers; +{ + TODO: + - adjust database to new song-loading (e.g. use SongIDs) + - support for deletion of outdated covers + - support for update of changed covers + - use paths relative to the song for removable disks support + (a drive might have a different drive-name the next time it is connected, + so "H:/songs/..." will not match "I:/songs/...") +} + interface {$IFDEF FPC} @@ -8,255 +18,413 @@ interface {$I switches.inc} -uses gl, - Math, - Classes, - SysUtils, - {$IFNDEF FPC} - Graphics, - {$ENDIF} - UThemes, - UTexture; +uses + sdl, + SQLite3, + SQLiteTable3, + SysUtils, + Classes, + UImage, + UTexture; type - TCover = record - Name: string; - W: word; - H: word; - Size: integer; - Position: integer; // position of picture in the cache file -// Data: array of byte; + ECoverDBException = class(Exception) + end; + + TCover = class + private + ID: int64; + Filename: WideString; + public + constructor Create(ID: int64; Filename: WideString); + function GetPreviewTexture(): TTexture; + function GetTexture(): TTexture; end; - TCovers = class - Cover: array of TCover; - W: word; - H: word; - Size: integer; - Data: array of byte; - WritetoFile: Boolean; - - constructor Create; - procedure Load; - procedure Save; - procedure AddCover(Name: string); - function CoverExists(Name: string): boolean; - function CoverNumber(Name: string): integer; - procedure PrepareData(Name: string); + TThumbnailInfo = record + CoverWidth: integer; // Original width of cover + CoverHeight: integer; // Original height of cover + PixelFormat: TImagePixelFmt; // Pixel-format of thumbnail + end; + + TCoverDatabase = class + private + DB: TSQLiteDatabase; + procedure InitCoverDatabase(); + function CreateThumbnail(const Filename: WideString; var Info: TThumbnailInfo): PSDL_Surface; + function LoadCover(CoverID: int64): TTexture; + procedure DeleteCover(CoverID: int64); + function FindCoverIntern(const Filename: WideString): int64; + procedure Open(); + function GetVersion(): integer; + procedure SetVersion(Version: integer); + public + constructor Create(); + destructor Destroy; override; + function AddCover(const Filename: WideString): TCover; + function FindCover(const Filename: WideString): TCover; + function CoverExists(const Filename: WideString): boolean; + function GetMaxCoverSize(): integer; + procedure SetMaxCoverSize(Size: integer); + end; + + TBlobWrapper = class(TCustomMemoryStream) + function Write(const Buffer; Count: Integer): Integer; override; end; var - Covers: TCovers; + Covers: TCoverDatabase; implementation -uses UMain, - // UFiles, - ULog, - DateUtils; - -constructor TCovers.Create; +uses + UMain, + ULog, + UPlatform, + UIni, + Math, + DateUtils; + +const + COVERDB_FILENAME = 'cover.db'; + COVERDB_VERSION = 01; // 0.1 + COVER_TBL = 'Cover'; + COVER_THUMBNAIL_TBL = 'CoverThumbnail'; + COVER_IDX = 'Cover_Filename_IDX'; + +// Note: DateUtils.DateTimeToUnix() will throw an exception in FPC +function DateTimeToUnixTime(time: TDateTime): int64; begin - inherited; - W := 128; - H := 128; - Size := W*H*3; - Load; - WritetoFile := True; + Result := Round((time - UnixDateDelta) * SecsPerDay); end; -procedure TCovers.Load; -var - F: File; - C: integer; // cover number - W: word; - H: word; - Bits: byte; - NLen: word; - Name: string; -// Data: array of byte; +// Note: DateUtils.UnixToDateTime() will throw an exception in FPC +function UnixTimeToDateTime(timestamp: int64): TDateTime; begin - if FileExists(GamePath + 'covers.cache') then - begin - AssignFile(F, GamePath + 'covers.cache'); - Reset(F, 1); + Result := timestamp / SecsPerDay + UnixDateDelta; +end; - WritetoFile := not FileIsReadOnly(GamePath + 'covers.cache'); - SetLength(Cover, 0); +{ TBlobWrapper } - while not EOF(F) do - begin - SetLength(Cover, Length(Cover)+1); +function TBlobWrapper.Write(const Buffer; Count: Integer): Integer; +begin + SetPointer(Pointer(Buffer), Count); + Result := Count; +end; - BlockRead(F, W, 2); - Cover[High(Cover)].W := W; - BlockRead(F, H, 2); - Cover[High(Cover)].H := H; +{ TCover } - BlockRead(F, Bits, 1); +constructor TCover.Create(ID: int64; Filename: WideString); +begin + Self.ID := ID; + Self.Filename := Filename; +end; - Cover[High(Cover)].Size := W * H * (Bits div 8); +function TCover.GetPreviewTexture(): TTexture; +begin + Result := Covers.LoadCover(ID); +end; - // test - // W := 128; - // H := 128; - // Bits := 24; - // Seek(F, FilePos(F) + 3); +function TCover.GetTexture(): TTexture; +begin + Result := Texture.LoadTexture(Filename); +end; - BlockRead(F, NLen, 2); - SetLength(Name, NLen); - BlockRead(F, Name[1], NLen); - Cover[High(Cover)].Name := Name; +{ TCoverDatabase } - Cover[High(Cover)].Position := FilePos(F); - Seek(F, FilePos(F) + W*H*(Bits div 8)); +constructor TCoverDatabase.Create(); +begin + inherited; - // SetLength(Cover[High(Cover)].Data, W*H*(Bits div 8)); - // BlockRead(F, Cover[High(Cover)].Data[0], W*H*(Bits div 8)); + Open(); + InitCoverDatabase(); +end; - end; // While +destructor TCoverDatabase.Destroy; +begin + DB.Free; + inherited; +end; - CloseFile(F); - end; // fileexists +function TCoverDatabase.GetVersion(): integer; +begin + Result := DB.GetTableValue('PRAGMA user_version'); end; -procedure TCovers.Save; +procedure TCoverDatabase.SetVersion(Version: integer); +begin + DB.ExecSQL(Format('PRAGMA user_version = %d', [Version])); +end; + +function TCoverDatabase.GetMaxCoverSize(): integer; +begin + Result := ITextureSizeVals[Ini.TextureSize]; +end; + +procedure TCoverDatabase.SetMaxCoverSize(Size: integer); var - F: File; - C: integer; // cover number - W: word; - H: word; - NLen: word; - Bits: byte; + I: integer; begin -{ AssignFile(F, GamePath + 'covers.cache'); - Rewrite(F, 1); - - Bits := 24; - for C := 0 to High(Cover) do begin - W := Cover[C].W; - H := Cover[C].H; - - BlockWrite(F, W, 2); - BlockWrite(F, H, 2); - BlockWrite(F, Bits, 1); - - NLen := Length(Cover[C].Name); - BlockWrite(F, NLen, 2); - BlockWrite(F, Cover[C].Name[1], NLen); - BlockWrite(F, Cover[C].Data[0], W*H*(Bits div 8)); + // search for first valid cover-size > Size + for I := 0 to Length(ITextureSizeVals)-1 do + begin + if (Size <= ITextureSizeVals[I]) then + begin + Ini.TextureSize := I; + Exit; + end; end; - CloseFile(F);} + // fall-back to highest size + Ini.TextureSize := High(ITextureSizeVals); end; -procedure TCovers.AddCover(Name: string); +procedure TCoverDatabase.Open(); var - B: integer; - F: File; - C: integer; // cover number - NLen: word; - Bits: byte; + Version: integer; + Filename: string; begin - if not CoverExists(Name) then + Filename := UTF8Encode(Platform.GetGameUserPath() + COVERDB_FILENAME); + + DB := TSQLiteDatabase.Create(Filename); + Version := GetVersion(); + + // check version, if version is too old/new, delete database file + if ((Version <> 0) and (Version <> COVERDB_VERSION)) then begin - SetLength(Cover, Length(Cover)+1); - Cover[High(Cover)].Name := Name; + Log.LogInfo('Outdated cover-database file found', 'TCoverDatabase.Open'); + // close and delete outdated file + DB.Free; + if (not DeleteFile(Filename)) then + raise ECoverDBException.Create('Could not delete ' + Filename); + // reopen + DB := TSQLiteDatabase.Create(Filename); + Version := 0; + end; - Cover[High(Cover)].W := W; - Cover[High(Cover)].H := H; - Cover[High(Cover)].Size := Size; + // set version number after creation + if (Version = 0) then + SetVersion(COVERDB_VERSION); - // do not copy data. write them directly to file -// SetLength(Cover[High(Cover)].Data, Size); -// for B := 0 to Size-1 do -// Cover[High(Cover)].Data[B] := CacheMipmap[B]; + // speed-up disk-writing. The default FULL-synchronous mode is too slow. + // With this option disk-writing is approx. 4 times faster but the database + // might be corrupted if the OS crashes, although this is very unlikely. + DB.ExecSQL('PRAGMA synchronous = OFF;'); + + // the next line rather gives a slow-down instead of a speed-up, so we do not use it + //DB.ExecSQL('PRAGMA temp_store = MEMORY;'); +end; - if WritetoFile then - begin - AssignFile(F, GamePath + 'covers.cache'); - - if FileExists(GamePath + 'covers.cache') then - begin - Reset(F, 1); - Seek(F, FileSize(F)); - end - else - begin - Rewrite(F, 1); - end; - - Bits := 24; - - BlockWrite(F, W, 2); - BlockWrite(F, H, 2); - BlockWrite(F, Bits, 1); - - NLen := Length(Name); - BlockWrite(F, NLen, 2); - BlockWrite(F, Name[1], NLen); - - Cover[High(Cover)].Position := FilePos(F); - //BlockWrite(F, CacheMipmap[0], W*H*(Bits div 8)); - - CloseFile(F); - end; - end - else - Cover[High(Cover)].Position := 0; +procedure TCoverDatabase.InitCoverDatabase(); +begin + DB.ExecSQL('CREATE TABLE IF NOT EXISTS ['+COVER_TBL+'] (' + + '[ID] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ' + + '[Filename] TEXT UNIQUE NOT NULL, ' + + '[Date] INTEGER NOT NULL, ' + + '[Width] INTEGER NOT NULL, ' + + '[Height] INTEGER NOT NULL ' + + ')'); + + DB.ExecSQL('CREATE INDEX IF NOT EXISTS ['+COVER_IDX+'] ON ['+COVER_TBL+'](' + + '[Filename] ASC' + + ')'); + + DB.ExecSQL('CREATE TABLE IF NOT EXISTS ['+COVER_THUMBNAIL_TBL+'] (' + + '[ID] INTEGER NOT NULL PRIMARY KEY, ' + + '[Format] INTEGER NOT NULL, ' + + '[Width] INTEGER NOT NULL, ' + + '[Height] INTEGER NOT NULL, ' + + '[Data] BLOB NULL' + + ')'); end; -function TCovers.CoverExists(Name: string): boolean; +function TCoverDatabase.FindCoverIntern(const Filename: WideString): int64; +begin + Result := DB.GetTableValue('SELECT [ID] FROM ['+COVER_TBL+'] ' + + 'WHERE [Filename] = ?', + [UTF8Encode(Filename)]); +end; + +function TCoverDatabase.FindCover(const Filename: WideString): TCover; var - C: integer; // cover + CoverID: int64; +begin + Result := nil; + try + CoverID := FindCoverIntern(Filename); + if (CoverID > 0) then + Result := TCover.Create(CoverID, Filename); + except on E: Exception do + Log.LogError(E.Message, 'TCoverDatabase.FindCover'); + end; +end; + +function TCoverDatabase.CoverExists(const Filename: WideString): boolean; begin Result := false; - C := 0; - - while (C <= High(Cover)) and (Result = false) do - begin - if Cover[C].Name = Name then - Result := true; - - Inc(C); + try + Result := (FindCoverIntern(Filename) > 0); + except on E: Exception do + Log.LogError(E.Message, 'TCoverDatabase.CoverExists'); end; end; -function TCovers.CoverNumber(Name: string): integer; +function TCoverDatabase.AddCover(const Filename: WideString): TCover; var - C: integer; + CoverID: int64; + Thumbnail: PSDL_Surface; + CoverData: TBlobWrapper; + FileDate: TDateTime; + Info: TThumbnailInfo; begin - Result := -1; - C := 0; - - while (C <= High(Cover)) and (Result = -1) do - begin - if Cover[C].Name = Name then - Result := C; - - Inc(C); + Result := nil; + + //if (not FileExists(Filename)) then + // Exit; + + // TODO: replace '\' with '/' in filename + FileDate := Now(); //FileDateToDateTime(FileAge(Filename)); + + Thumbnail := CreateThumbnail(Filename, Info); + if (Thumbnail = nil) then + Exit; + + CoverData := TBlobWrapper.Create; + CoverData.Write(Thumbnail^.pixels, Thumbnail^.h * Thumbnail^.pitch); + + try + // Note: use a transaction to speed-up file-writing. + // Without data written by the first INSERT might be moved at the second INSERT. + DB.BeginTransaction(); + + // add general cover info + DB.ExecSQL('INSERT INTO ['+COVER_TBL+'] ' + + '([Filename], [Date], [Width], [Height]) VALUES' + + '(?, ?, ?, ?)', + [UTF8Encode(Filename), DateTimeToUnixTime(FileDate), + Info.CoverWidth, Info.CoverHeight]); + + // get auto-generated cover ID + CoverID := DB.GetLastInsertRowID(); + + // add thumbnail info + DB.ExecSQL('INSERT INTO ['+COVER_THUMBNAIL_TBL+'] ' + + '([ID], [Format], [Width], [Height], [Data]) VALUES' + + '(?, ?, ?, ?, ?)', + [CoverID, Ord(Info.PixelFormat), + Thumbnail^.w, Thumbnail^.h, CoverData]); + + Result := TCover.Create(CoverID, Filename); + except on E: Exception do + Log.LogError(E.Message, 'TCoverDatabase.AddCover'); + end; + + DB.Commit(); + CoverData.Free; + SDL_FreeSurface(Thumbnail); +end; + +function TCoverDatabase.LoadCover(CoverID: int64): TTexture; +var + Width, Height: integer; + PixelFmt: TImagePixelFmt; + Data: PChar; + DataSize: integer; + Filename: WideString; + Table: TSQLiteUniTable; +begin + Table := nil; + + try + Table := DB.GetUniTable(Format( + 'SELECT C.[Filename], T.[Format], T.[Width], T.[Height], T.[Data] ' + + 'FROM ['+COVER_TBL+'] C ' + + 'INNER JOIN ['+COVER_THUMBNAIL_TBL+'] T ' + + 'USING(ID) ' + + 'WHERE [ID] = %d', [CoverID])); + + Filename := UTF8Decode(Table.FieldAsString(0)); + PixelFmt := TImagePixelFmt(Table.FieldAsInteger(1)); + Width := Table.FieldAsInteger(2); + Height := Table.FieldAsInteger(3); + + Data := Table.FieldAsBlobPtr(4, DataSize); + if (Data <> nil) and + (PixelFmt = ipfRGB) then + begin + Result := Texture.CreateTexture(Data, Filename, Width, Height, 24) + end + else + begin + FillChar(Result, SizeOf(TTexture), 0); + end; + except on E: Exception do + Log.LogError(E.Message, 'TCoverDatabase.LoadCover'); end; + + Table.Free; end; -procedure TCovers.PrepareData(Name: string); +procedure TCoverDatabase.DeleteCover(CoverID: int64); +begin + DB.ExecSQL(Format('DELETE FROM ['+COVER_TBL+'] WHERE [ID] = %d', [CoverID])); + DB.ExecSQL(Format('DELETE FROM ['+COVER_THUMBNAIL_TBL+'] WHERE [ID] = %d', [CoverID])); +end; + +(** + * Returns a pointer to an array of bytes containing the texture data in the + * requested size + *) +function TCoverDatabase.CreateThumbnail(const Filename: WideString; var Info: TThumbnailInfo): PSDL_Surface; var - F: File; - C: integer; + TargetAspect, SourceAspect: double; + //TargetWidth, TargetHeight: integer; + Thumbnail: PSDL_Surface; + MaxSize: integer; begin - if FileExists(GamePath + 'covers.cache') then + Result := nil; + + MaxSize := GetMaxCoverSize(); + + Thumbnail := LoadImage(Filename); + if (not assigned(Thumbnail)) then begin - AssignFile(F, GamePath + 'covers.cache'); - Reset(F, 1); - - C := CoverNumber(Name); - SetLength(Data, Cover[C].Size); - Seek(F, Cover[C].Position); - BlockRead(F, Data[0], Cover[C].Size); - CloseFile(F); + Log.LogError('Could not load cover: "'+ Filename +'"', 'TCoverDatabase.AddCover'); + Exit; end; + + // Convert pixel format as needed + AdjustPixelFormat(Thumbnail, TEXTURE_TYPE_PLAIN); + + Info.CoverWidth := Thumbnail^.w; + Info.CoverHeight := Thumbnail^.h; + Info.PixelFormat := ipfRGB; + + (* TODO: keep aspect ratio + TargetAspect := Width / Height; + SourceAspect := TexSurface.w / TexSurface.h; + + // Scale texture to covers dimensions (keep aspect) + if (SourceAspect >= TargetAspect) then + begin + TargetWidth := Width; + TargetHeight := Trunc(Width / SourceAspect); + end + else + begin + TargetHeight := Height; + TargetWidth := Trunc(Height * SourceAspect); + end; + *) + + // TODO: do not scale if image is smaller + ScaleImage(Thumbnail, MaxSize, MaxSize); + + Result := Thumbnail; end; end. + diff --git a/Game/Code/Classes/UIni.pas b/Game/Code/Classes/UIni.pas index 77e7a745..2c01a652 100644 --- a/Game/Code/Classes/UIni.pas +++ b/Game/Code/Classes/UIni.pas @@ -154,7 +154,10 @@ const IScreens: array[0..1] of string = ('1', '2'); IFullScreen: array[0..1] of string = ('Off', 'On'); IDepth: array[0..1] of string = ('16 bit', '32 bit'); - ITextureSize: array[0..2] of string = ('128', '256', '512'); + + ITextureSize: array[0..2] of string = ('128', '256', '512'); + ITextureSizeVals: array[0..2] of integer = ( 128, 256, 512); + ISingWindow: array[0..1] of string = ('Small', 'Big'); //SingBar Mod diff --git a/Game/Code/Classes/UMain.pas b/Game/Code/Classes/UMain.pas index 82456072..d0c702b9 100644 --- a/Game/Code/Classes/UMain.pas +++ b/Game/Code/Classes/UMain.pas @@ -251,7 +251,7 @@ begin // Covers Cache Log.BenchmarkStart(1); Log.LogStatus('Creating Covers Cache', 'Initialization'); - Covers := TCovers.Create; + Covers := TCoverDatabase.Create; Log.LogBenchmark('Loading Covers Cache Array', 1); Log.BenchmarkStart(1); diff --git a/Game/Code/Classes/USongs.pas b/Game/Code/Classes/USongs.pas index 8de78777..783b1012 100644 --- a/Game/Code/Classes/USongs.pas +++ b/Game/Code/Classes/USongs.pas @@ -206,8 +206,8 @@ begin if assigned( CatCovers ) then CatCovers.Load; - if assigned( Covers ) then - Covers.Load; + //if assigned( Covers ) then + // Covers.Load; if assigned(ScreenSong) then begin diff --git a/Game/Code/Screens/UScreenSong.pas b/Game/Code/Screens/UScreenSong.pas index 53271778..1b12d4b4 100644 --- a/Game/Code/Screens/UScreenSong.pas +++ b/Game/Code/Screens/UScreenSong.pas @@ -143,6 +143,7 @@ uses UDLLManager, UParty, UPlaylist, + UMenuButton, UScreenSongMenu; // ***** Public methods ****** // @@ -795,8 +796,13 @@ end; procedure TScreenSong.GenerateThumbnails(); var I: Integer; - LoadNoCover: boolean; OldTextureLimit: integer; + CoverButtonIndex: integer; + CoverButton: TButton; + CoverName: string; + CoverTexture: TTexture; + Cover: TCover; + Song: TSong; begin if (Length(CatSongs.Song) <= 0) then Exit; @@ -804,71 +810,49 @@ begin // set length of button array once instead for every song SetButtonLength(Length(CatSongs.Song)); - // backup and set texture limit - OldTextureLimit := Texture.Limit; - Texture.Limit := 512; - // create all buttons for I := 0 to High(CatSongs.Song) do begin - // if cover not found then show 'no cover' - if (not FileExists(CatSongs.Song[I].Path + CatSongs.Song[I].Cover)) then - CatSongs.Song[I].Cover := ''; + CoverButton := nil; - if (CatSongs.Song[I].Cover = '') then - begin - LoadNoCover := true; - end - else - begin - // cache texture if there is a need to this - if (not Covers.CoverExists(CatSongs.Song[I].Path + CatSongs.Song[I].Cover)) then - begin - Texture.GetTexture(CatSongs.Song[I].Path + CatSongs.Song[I].Cover, TEXTURE_TYPE_PLAIN, true); // preloads textures and creates cache mipmap + // create a clickable cover + CoverButtonIndex := AddButton(300 + I*250, 140, 200, 200, '', TEXTURE_TYPE_PLAIN, Theme.Song.Cover.Reflections); + if (CoverButtonIndex > -1) then + CoverButton := Button[CoverButtonIndex]; + if (CoverButton = nil) then + Continue; - // puts this texture to the cache file - Covers.AddCover(CatSongs.Song[I].Path + CatSongs.Song[I].Cover); + Song := CatSongs.Song[I]; - // unload full size texture - Texture.UnloadTexture(CatSongs.Song[I].Path + CatSongs.Song[I].Cover, TEXTURE_TYPE_PLAIN, false); - - // TODO: we should also add mipmap texture by calling createtexture and use mipmap cache as data source - end; + // if cover-image is not found then show 'no cover' + if (not FileExists(Song.Path + Song.Cover)) then + Song.Cover := ''; - // and now load it from cache file (small place for the optimization by eliminating reading it from file, but not here) - try - // FIXME: do we really need try-except here? AddButton does not seem to throw exceptions if it fails. - AddButton(300 + I*250, 140, 200, 200, CatSongs.Song[I].Path + CatSongs.Song[I].Cover, TEXTURE_TYPE_PLAIN, Theme.Song.Cover.Reflections); - LoadNoCover := false; - except - // report error and change cover to NoCover - Log.LogError('Could not load Cover: ' + CatSongs.Song[I].Cover); - LoadNoCover := true; - end; - end; - - if (LoadNoCover) then + if (Song.Cover = '') then + CoverName := Skin.GetTextureFileName('SongCover') + else + CoverName := Song.Path + Song.Cover; + + // load cover and cache its texture + Cover := Covers.FindCover(CoverName); + if (Cover = nil) then + Cover := Covers.AddCover(CoverName); + + // use the cached texture + // TODO: this is a workaround until the new song-loading works. + // The TCover object should be added to the song-object. The thumbnails + // should be loaded each time the song-screen is shown (it is real fast). + // This way, we will not waste that much memory and have a link between + // song and cover. + if (Cover <> nil) then begin - try - // FIXME: do we really need try-except here? AddButton does not seem to throw exceptions if it fails. - AddButton(300 + I*250, 140, 200, 200, Skin.GetTextureFileName('SongCover'), TEXTURE_TYPE_PLAIN, Theme.Song.Cover.Reflections) - except - // NoSong Cover is damaged - Log.LogError('NoCover Cover is damaged!'); - // set a dummy cover and start loading next Song - try - // FIXME: do we really need try-except here? AddButton does not seem to throw exceptions if it fails. - AddButton(300 + I*250, 140, 200, 200, '', TEXTURE_TYPE_PLAIN, Theme.Song.Cover.Reflections); - except - ShowMessage('"No Cover" image is damaged. Ultrastar will exit now.'); - Halt; - end; - end; + CoverTexture := Cover.GetPreviewTexture(); + Texture.AddTexture(CoverTexture, TEXTURE_TYPE_PLAIN, true); + CoverButton.Texture := CoverTexture; end; - end; - // restore texture limit - Texture.Limit := OldTextureLimit; + Cover.Free; + end; end; procedure TScreenSong.SetScroll; -- cgit v1.2.3