1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
|
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
//=============================================================================
//
// sprite caching system
//
//=============================================================================
#include "common/system.h"
#include "ags/shared/core/platform.h"
#include "ags/shared/util/stream.h"
#include "common/std/algorithm.h"
#include "ags/shared/ac/sprite_cache.h"
#include "ags/shared/ac/game_struct_defines.h"
#include "ags/shared/debugging/out.h"
#include "ags/shared/gfx/bitmap.h"
#include "ags/globals.h"
namespace AGS3 {
using namespace AGS::Shared;
// Tells that the sprite is found in the game resources.
#define SPRCACHEFLAG_ISASSET 0x01
// Tells that the sprite is assigned externally and cannot be autodisposed.
#define SPRCACHEFLAG_EXTERNAL 0x02
// Tells that the asset sprite failed to load
#define SPRCACHEFLAG_ERROR 0x04
// Locked sprites are ones that should not be freed when out of cache space.
#define SPRCACHEFLAG_LOCKED 0x08
// High-verbosity sprite cache log
#if DEBUG_SPRITECACHE
#define SprCacheLog(...) Debug::Printf(kDbgGroup_SprCache, kDbgMsg_Debug, __VA_ARGS__)
#else
#define SprCacheLog(...)
#endif
namespace AGS {
namespace Shared {
SpriteCache::SpriteCache(std::vector<SpriteInfo> &sprInfos, const Callbacks &callbacks)
: _sprInfos(sprInfos), _maxCacheSize(DEFAULTCACHESIZE_KB * 1024u),
_cacheSize(0u), _lockedSize(0u) {
_callbacks.AdjustSize = (callbacks.AdjustSize) ? callbacks.AdjustSize : DummyAdjustSize;
_callbacks.InitSprite = (callbacks.InitSprite) ? callbacks.InitSprite : DummyInitSprite;
_callbacks.PostInitSprite = (callbacks.PostInitSprite) ? callbacks.PostInitSprite : DummyPostInitSprite;
_callbacks.PrewriteSprite = (callbacks.PrewriteSprite) ? callbacks.PrewriteSprite : DummyPrewriteSprite;
// Generate a placeholder sprite: 1x1 transparent bitmap
_placeholder.reset(BitmapHelper::CreateTransparentBitmap(1, 1, 8));
}
size_t SpriteCache::GetCacheSize() const {
return _cacheSize;
}
size_t SpriteCache::GetLockedSize() const {
return _lockedSize;
}
size_t SpriteCache::GetMaxCacheSize() const {
return _maxCacheSize;
}
size_t SpriteCache::GetSpriteSlotCount() const {
return _spriteData.size();
}
void SpriteCache::SetMaxCacheSize(size_t size) {
FreeMem(size);
_maxCacheSize = size;
}
bool SpriteCache::HasFreeSlots() const {
return !((_spriteData.size() == SIZE_MAX) || (_spriteData.size() > MAX_SPRITE_INDEX));
}
bool SpriteCache::IsAssetSprite(sprkey_t index) const {
return index >= 0 && (size_t)index < _spriteData.size() && // in the valid range
_spriteData[index].IsAssetSprite(); // found in the game resources
}
void SpriteCache::Reset() {
_file.Close();
_spriteData.clear();
_mru.clear();
_cacheSize = 0;
_lockedSize = 0;
}
bool SpriteCache::SetSprite(sprkey_t index, std::unique_ptr<Bitmap> image, int flags) {
if (index < 0 || EnlargeTo(index) != index) {
Debug::Printf(kDbgGroup_SprCache, kDbgMsg_Error, "SetSprite: unable to use index %d", index);
return false;
}
if (!image || image->GetSize().IsNull() || image->GetColorDepth() <= 0) {
DisposeSprite(index); // free previous item in this slot anyway
Debug::Printf(kDbgGroup_SprCache, kDbgMsg_Error, "SetSprite: attempt to assign an invalid bitmap to index %d", index);
return false;
}
const int spf_flags = flags
| (SPF_HICOLOR * image->GetColorDepth() > 8)
| (SPF_TRUECOLOR * image->GetColorDepth() > 16);
_sprInfos[index] = SpriteInfo(image->GetWidth(), image->GetHeight(), spf_flags);
// Assign sprite with 0 size, as it will not be included into the cache size
_spriteData[index] = SpriteData(image.release(), 0, SPRCACHEFLAG_EXTERNAL | SPRCACHEFLAG_LOCKED);
SprCacheLog("SetSprite: (external) %d", index);
return true;
}
void SpriteCache::SetEmptySprite(sprkey_t index, bool as_asset) {
if (index < 0 || EnlargeTo(index) != index) {
Debug::Printf(kDbgGroup_SprCache, kDbgMsg_Error, "SetEmptySprite: unable to use index %d", index);
return;
}
if (as_asset)
_spriteData[index].Flags = SPRCACHEFLAG_ISASSET;
RemapSpriteToPlaceholder(index);
}
Bitmap *SpriteCache::RemoveSprite(sprkey_t index) {
if (index < 0 || (size_t)index >= _spriteData.size())
return nullptr;
Bitmap *image = _spriteData[index].Image.release();
InitNullSprite(index);
SprCacheLog("RemoveSprite: %d", index);
return image;
}
void SpriteCache::DisposeSprite(sprkey_t index) {
assert(index >= 0); // out of positive range indexes are valid to fail
if (index < 0 || (size_t)index >= _spriteData.size())
return;
InitNullSprite(index);
SprCacheLog("RemoveAndDispose: %d", index);
}
sprkey_t SpriteCache::EnlargeTo(sprkey_t topmost) {
if (topmost < 0 || topmost > MAX_SPRITE_INDEX)
return -1;
if ((size_t)topmost < _spriteData.size())
return topmost;
size_t newsize = topmost + 1;
_sprInfos.resize(newsize);
_spriteData.resize(newsize);
return topmost;
}
sprkey_t SpriteCache::GetFreeIndex() {
// FIXME: inefficient if large number of sprites were created in game;
// use "available ids" stack, see managed pool for an example;
// NOTE: this is shared with the Editor, which means we cannot rely on the
// number of "static" sprites and search for slots after... this may be
// resolved by splitting SpriteCache class further on "cache builder" and
// "runtime cache".
for (size_t i = MIN_SPRITE_INDEX; i < _spriteData.size(); ++i) {
// slot empty
if (!DoesSpriteExist(i)) {
_sprInfos[i] = SpriteInfo();
_spriteData[i] = SpriteData();
return i;
}
}
// enlarge the sprite bank to find a free slot and return the first new free slot
return EnlargeTo(_spriteData.size());
}
bool SpriteCache::SpriteData::DoesSpriteExist() const {
return (Image != nullptr) || // HAS loaded bitmap
((Flags & SPRCACHEFLAG_ISASSET) != 0); // OR found in the game resources
}
bool SpriteCache::SpriteData::IsAssetSprite() const {
return (Flags & SPRCACHEFLAG_ISASSET) != 0;
}
bool SpriteCache::SpriteData::IsError() const {
return (Flags & SPRCACHEFLAG_ERROR) != 0;
}
bool SpriteCache::SpriteData::IsExternalSprite() const {
return (Flags & SPRCACHEFLAG_EXTERNAL) != 0;
}
bool SpriteCache::SpriteData::IsLocked() const {
return (Flags & SPRCACHEFLAG_LOCKED) != 0;
}
bool SpriteCache::DoesSpriteExist(sprkey_t index) const {
return (index >= 0 && (size_t)index < _spriteData.size()) && // in the valid range
_spriteData[index].IsValid(); // has assigned sprite
}
Size SpriteCache::GetSpriteResolution(sprkey_t index) const {
return DoesSpriteExist(index) ? _sprInfos[index].GetResolution() : Size();
}
Bitmap *SpriteCache::operator[](sprkey_t index) {
// invalid sprite slot
if (!DoesSpriteExist(index) || _spriteData[index].IsError())
return _placeholder.get();
// Externally added sprite or locked sprite, don't put it into MRU list
if (_spriteData[index].IsExternalSprite() || _spriteData[index].IsLocked())
return _spriteData[index].Image.get();
// Either use ready image, or load one from assets
if (_spriteData[index].Image) {
// Move to the beginning of the MRU list
_mru.splice(_mru.begin(), _mru, _spriteData[index].MruIt);
return _spriteData[index].Image.get();
} else {
// Sprite exists in file but is not in mem, load it and add to MRU list
if (LoadSprite(index)) {
_spriteData[index].MruIt = _mru.insert(_mru.begin(), index);
return _spriteData[index].Image.get();
}
}
return _placeholder.get();
}
void SpriteCache::FreeMem(size_t space) {
for (int tries = 0; (_mru.size() > 0) && (_cacheSize >= (_maxCacheSize - space)); ++tries) {
DisposeOldest();
if (tries > 1000) { // ???
Debug::Printf(kDbgGroup_SprCache, kDbgMsg_Error, "RUNTIME CACHE ERROR: STUCK IN FREE_UP_MEM; RESETTING CACHE");
DisposeAllCached();
}
}
}
void SpriteCache::DisposeOldest() {
assert(_mru.size() > 0);
if (_mru.size() == 0)
return;
auto it = std::prev(_mru.end());
const auto sprnum = *it;
// Safety check: must be a sprite from resources
// TODO: compare with latest upstream
// Commented out the assertion, since it triggers for sprites that are in the list but remapped to the placeholder (sprite 0)
// Whispers of a Machine is affected by this issue (see TRAC #14730)
// assert(_spriteData[sprnum].IsAssetSprite());
if (!_spriteData[sprnum].IsAssetSprite()) {
Debug::Printf(kDbgGroup_SprCache, kDbgMsg_Error, "SpriteCache::DisposeOldest: in MRU list sprite %d is external or does not exist", sprnum);
_mru.erase(it);
// std::list::erase() invalidates iterators to the erased item.
// But our implementation does not.
_spriteData[sprnum].MruIt._node = nullptr;
return;
}
// Delete the image, unless is locked
// NOTE: locked sprites may still occur in MRU list
if (!_spriteData[sprnum].IsLocked()) {
_cacheSize -= _spriteData[sprnum].Size;
_spriteData[sprnum].Image.reset();
SprCacheLog("DisposeOldest: disposed %d, size now %d KB", sprnum, _cacheSize / 1024);
}
// Remove from the mru list
_mru.erase(it);
// std::list::erase() invalidates iterators to the erased item.
// But our implementation does not.
_spriteData[sprnum].MruIt._node = nullptr;
}
void SpriteCache::DisposeAllCached() {
for (size_t i = 0; i < _spriteData.size(); ++i) {
if (!_spriteData[i].IsLocked() && // not locked
_spriteData[i].IsAssetSprite()) // sprite from game resource
{
_spriteData[i].Image.reset();
}
}
_cacheSize = _lockedSize;
_mru.clear();
}
void SpriteCache::PrecacheSprite(sprkey_t index) {
if (index < 0 || (size_t)index >= _spriteData.size())
return;
if (!_spriteData[index].IsAssetSprite())
return; // cannot precache a non-asset sprite
size_t size = 0;
if (_spriteData[index].Image == nullptr) {
size = LoadSprite(index);
} else if (!_spriteData[index].IsLocked()) {
size = _spriteData[index].Size;
// Remove locked sprite from the MRU list
_mru.erase(_spriteData[index].MruIt);
// std::list::erase() invalidates iterators to the erased item.
// But our implementation does not.
_spriteData[index].MruIt._node = nullptr;
}
// make sure locked sprites can't fill the cache
_maxCacheSize += size;
_lockedSize += size;
_spriteData[index].Flags |= SPRCACHEFLAG_LOCKED;
SprCacheLog("Precached %d", index);
}
void SpriteCache::LockSprite(sprkey_t index) {
assert(index >= 0); // out of positive range indexes are valid to fail
if (index < 0 || (size_t)index >= _spriteData.size())
return;
if (!_spriteData[index].IsAssetSprite())
return; // cannot lock a non-asset sprite
if (_spriteData[index].DoesSpriteExist()) {
_spriteData[index].Flags |= SPRCACHEFLAG_LOCKED;
} else {
LoadSprite(index, true);
}
SprCacheLog("Locked %d", index);
}
void SpriteCache::UnlockSprite(sprkey_t index) {
assert(index >= 0); // out of positive range indexes are valid to fail
if (index < 0 || (size_t)index >= _spriteData.size())
return;
if (!_spriteData[index].IsAssetSprite() ||
!_spriteData[index].IsLocked())
return; // cannot unlock a non-asset sprite, or non-locked sprite
_spriteData[index].Flags &= ~SPRCACHEFLAG_LOCKED;
SprCacheLog("Unlocked %d", index);
}
size_t SpriteCache::LoadSprite(sprkey_t index, bool lock) {
assert((index >= 0) && ((size_t)index < _spriteData.size()));
if (index < 0 || (size_t)index >= _spriteData.size())
return 0;
assert((_spriteData[index].Flags & SPRCACHEFLAG_ISASSET) != 0);
Bitmap *image;
HError err = _file.LoadSprite(index, image);
if (!image) {
Debug::Printf(kDbgGroup_SprCache, kDbgMsg_Warn,
"LoadSprite: failed to load sprite %d:\n%s\n - remapping to placeholder", index,
err ? "Sprite does not exist." : err->FullMessage().GetCStr());
RemapSpriteToPlaceholder(index);
return 0;
}
// Let the external user convert this sprite's image for their needs
image = _callbacks.InitSprite(index, image, _sprInfos[index].Flags);
if (!image) {
Debug::Printf(kDbgGroup_SprCache, kDbgMsg_Warn,
"LoadSprite: failed to initialize sprite %d, remapping to placeholder", index);
RemapSpriteToPlaceholder(index);
return 0;
}
// save the stored sprite info
_sprInfos[index].Width = image->GetWidth();
_sprInfos[index].Height = image->GetHeight();
// Clear up space before adding to cache
const size_t size = image->GetWidth() * image->GetHeight() * image->GetBPP();
FreeMem(size);
// Add to the cache, lock if requested or if it's sprite 0
const bool should_lock = lock || (index == 0);
_spriteData[index] = SpriteData(image, size, SPRCACHEFLAG_ISASSET);
_spriteData[index].Flags |= (SPRCACHEFLAG_LOCKED * should_lock);
_cacheSize += size;
SprCacheLog("Loaded %d, size now %zu KB", index, _cacheSize / 1024);
// Let the external user to react to the new sprite;
// note that this callback is allowed to modify the sprite's pixels,
// but not its size or flags.
_callbacks.PostInitSprite(index);
return size;
}
void SpriteCache::RemapSpriteToPlaceholder(sprkey_t index) {
assert((index > 0) && ((size_t)index < _spriteData.size()));
_sprInfos[index] = SpriteInfo(_placeholder->GetWidth(), _placeholder->GetHeight(), _placeholder->GetColorDepth());
_spriteData[index].Flags |= SPRCACHEFLAG_ERROR;
SprCacheLog("RemapSpriteToPlaceholder: %d", index);
}
void SpriteCache::InitNullSprite(sprkey_t index) {
assert(index >= 0);
_sprInfos[index] = SpriteInfo();
_spriteData[index] = SpriteData();
}
int SpriteCache::SaveToFile(const String &filename, int store_flags, SpriteCompression compress, SpriteFileIndex &index) {
std::vector<std::pair<bool, Bitmap *>> sprites;
for (size_t i = 0; i < _spriteData.size(); ++i) {
_callbacks.PrewriteSprite(_spriteData[i].Image.get());
sprites.push_back(std::make_pair(DoesSpriteExist(i), _spriteData[i].Image.get()));
}
return SaveSpriteFile(filename, sprites, &_file, store_flags, compress, index);
}
HError SpriteCache::InitFile(const String &filename, const String &sprindex_filename) {
Reset();
std::vector<Size> metrics;
HError err = _file.OpenFile(filename, sprindex_filename, metrics);
if (!err)
return err;
// Initialize sprite infos
size_t newsize = metrics.size();
_sprInfos.resize(newsize);
_spriteData.resize(newsize);
_mru.clear();
for (size_t i = 0; i < metrics.size(); ++i) {
if (!metrics[i].IsNull()) {
// Existing sprite
_spriteData[i].Flags = SPRCACHEFLAG_ISASSET;
Size newsz = _callbacks.AdjustSize(Size(metrics[i].Width, metrics[i].Height), _sprInfos[i].Flags);
_sprInfos[i].Width = newsz.Width;
_sprInfos[i].Height = newsz.Height;
} else {
// Mark as empty slot
InitNullSprite(i);
}
}
return HError::None();
}
void SpriteCache::DetachFile() {
_file.Close();
}
} // namespace Shared
} // namespace AGS
} // namespace AGS3
|