Skip to content

Commit

Permalink
Slightly optimize loading same-sprite monsters
Browse files Browse the repository at this point in the history
For monsters with the same sprite, load the sprite from the file system only once.

Example:

```
VERBOSE: Loaded monster graphics: falspear\phall   452 KiB   x1
VERBOSE: Loaded monster graphics: skelbow\sklbw    618 KiB   x1
VERBOSE: Loaded monster graphics: skelsd\sklsr     610 KiB   x1
VERBOSE: Loaded monster graphics: goatbow\goatb    832 KiB   x1
VERBOSE: Loaded monster graphics: bat\bat          282 KiB   x2 <-- here we only load the sprite once
VERBOSE: Loaded monster graphics: rhino\rhino     1306 KiB   x1
VERBOSE: Loaded monster graphics: golem\golem      298 KiB   x1
VERBOSE:  Total monster graphics:                 4401 KiB 4684 KiB
```

Here, the bat sprite will be loaded from the MPQ only once.
For the second sprite, we simply clone the first sprite before applying TRNs.

This also reduces the size of `MonsterData` from 88 bytes to 80.

When we migrate monster data to a TSV, the sprite IDs can be generated automatically at load time.
  • Loading branch information
glebm committed Oct 11, 2023
1 parent 706010e commit 8d6bf4f
Show file tree
Hide file tree
Showing 7 changed files with 423 additions and 208 deletions.
1 change: 1 addition & 0 deletions Source/levels/gendung.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ void LoadDungeonBase(const char *path, Point spawn, int floorId, int dirtId)
LoadTransparency(dunData.get());

SetMapMonsters(dunData.get(), Point(0, 0).megaToWorld());
InitAllMonsterGFX();
SetMapObjects(dunData.get(), 0, 0);
}

Expand Down
343 changes: 204 additions & 139 deletions Source/monstdat.cpp

Large diffs are not rendered by default.

76 changes: 75 additions & 1 deletion Source/monstdat.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,73 @@ enum class MonsterAvailability : uint8_t {
Retail,
};

enum class MonsterSpriteId : uint8_t {
Zombie = 0, // "zombie\\zombie"
FallenSpear, // "falspear\\phall"
FallenSword, // "falsword\\fall"
SkeletonAxe, // "skelaxe\\sklax"
SkeletonBow, // "skelbow\\sklbw"
SkeletonCaptain, // "skelsd\\sklsr"
SkeletonDemon, // "demskel\\demskl"
SkeletonKing, // "sking\\sking"
Scavenger, // "scav\\scav"
InvisibleLord, // "tsneak\\tsneak"
Hidden, // "sneak\\sneak"
GoatMace, // "goatmace\\goat"
GoatBow, // "goatbow\\goatb"
GoatLord, // "goatlord\\goatl"
Bat, // "bat\\bat"
AcidBeast, // "acid\\acid"
Overlord, // "fat\\fat"
Butcher, // "fatc\\fatc"
Wyrm, // "worm\\worm"
MagmaDemon, // "magma\\magma"
HornedDemon, // "rhino\\rhino"
StormRider, // "thin\\thin"
Incinerator, // "fireman\\firem"
DevilKinBrute, // "bigfall\\fallg"
Gargoyle, // "gargoyle\\gargo"
Slayer, // "mega\\mega"
Viper, // "snake\\snake"
BlackKnight, // "black\\black"
Shredded, // "unrav\\unrav"
Succubus, // "succ\\scbs"
Counselor, // "mage\\mage"
Golem, // "golem\\golem"
Diablo, // "diablo\\diablo"
ArchLitch, // "darkmage\\dmage"
Hellboar, // "fork\\fork"
Stinger, // "scorp\\scorp"
Psychorb, // "eye\\eye"
Arachnon, // "spider\\spider"
HorkSpawn, // "spawn\\spawn"
Venomtail, // "wscorp\\wscorp"
Necromorb, // "eye2\\eye2"
SpiderLord, // "bspidr\\bspidr"
Lashworm, // "clasp\\clasp"
Torchant, // "antworm\\worm"
HorkDemon, // "horkd\\horkd"
HellBug, // "hellbug\\hellbg"
Gravedigger, // "gravdg\\gravdg"
Rat, // "rat\\rat"
Firebat, // "hellbat\\helbat"
Lich, // "lich\\lich"
CryptDemon, // "bubba\\bubba"
Hellbat, // "hellbat2\\bhelbt"
ArchLich, // "lich2\\lich2"
Biclops, // "byclps\\byclps"
FleshThing, // "flesh\\flesh"
Reaper, // "reaper\\reap"
NaKrul, // "nkr\\nkr"
FIRST = Zombie,
LAST = NaKrul
};

struct MonsterData {
const char *name;
const char *assetsSuffix;
const char *soundSuffix;
const char *trnFile;
MonsterSpriteId spriteId;
MonsterAvailability availability;
uint16_t width;
uint16_t image;
Expand Down Expand Up @@ -129,6 +191,18 @@ struct MonsterData {
/** Using monster_treasure */
uint16_t treasure;
uint16_t exp;

[[nodiscard]] const char *spritePath() const;

[[nodiscard]] const char *soundPath() const
{
return soundSuffix != nullptr ? soundSuffix : spritePath();
}

[[nodiscard]] bool hasAnim(size_t index) const
{
return frames[index] != 0;
}
};

enum _monster_id : int16_t {
Expand Down
185 changes: 121 additions & 64 deletions Source/monster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
#include "utils/cl2_to_clx.hpp"
#include "utils/file_name_generator.hpp"
#include "utils/language.h"
#include "utils/log.hpp"
#include "utils/static_vector.hpp"
#include "utils/str_cat.hpp"
#include "utils/utf8.hpp"

Expand Down Expand Up @@ -97,15 +99,28 @@ size_t GetNumAnims(const MonsterData &monsterData)
return monsterData.hasSpecial ? 6 : 5;
}

size_t GetNumAnimsWithGraphics(const MonsterData &monsterData)
{
// Monster graphics can be missing for certain actions,
// e.g. Golem has no standing graphics.
const size_t numAnims = GetNumAnims(monsterData);
size_t result = 0;
for (size_t i = 0; i < numAnims; ++i) {
if (monsterData.hasAnim(i))
++result;
}
return result;
}

void InitMonsterTRN(CMonster &monst)
{
char path[64];
*BufCopy(path, "monsters\\", monst.data->trnFile, ".trn") = '\0';
*BufCopy(path, "monsters\\", monst.data().trnFile, ".trn") = '\0';
std::array<uint8_t, 256> colorTranslations;
LoadFileInMem(path, colorTranslations);
std::replace(colorTranslations.begin(), colorTranslations.end(), 255, 0);

const size_t numAnims = GetNumAnims(*monst.data);
const size_t numAnims = GetNumAnims(monst.data());
for (size_t i = 0; i < numAnims; i++) {
if (i == 1 && IsAnyOf(monst.type, MT_COUNSLR, MT_MAGISTR, MT_CABALIST, MT_ADVOCATE)) {
continue;
Expand Down Expand Up @@ -409,8 +424,19 @@ size_t AddMonsterType(_monster_id type, placeflag placeflag)
if (typeIndex == LevelMonsterTypeCount) {
LevelMonsterTypeCount++;
monsterType.type = type;
monstimgtot += MonstersData[type].image;
InitMonsterGFX(monsterType);
const MonsterData &monsterData = MonstersData[type];
monstimgtot += monsterData.image;

const size_t numAnims = GetNumAnims(monsterData);
for (size_t i = 0; i < numAnims; ++i) {
AnimStruct &anim = monsterType.anims[i];
anim.frames = monsterData.frames[i];
if (monsterData.hasAnim(i)) {
anim.rate = monsterData.rate[i];
anim.width = monsterData.width;
}
}

InitMonsterSND(monsterType);
}

Expand Down Expand Up @@ -3102,6 +3128,44 @@ bool UpdateModeStance(Monster &monster)
}
}

MonsterSpritesData LoadMonsterSpritesData(const MonsterData &monsterData)
{
const size_t numAnims = GetNumAnims(monsterData);

MonsterSpritesData result;
result.data = MultiFileLoader<MonsterSpritesData::MaxAnims> {}(
numAnims,
FileNameWithCharAffixGenerator({ "monsters\\", monsterData.spritePath() }, DEVILUTIONX_CL2_EXT, Animletter),
result.offsets.data(),
[&monsterData](size_t index) { return monsterData.hasAnim(index); });

#ifndef UNPACKED_MPQS
// Convert CL2 to CLX:
std::vector<std::vector<uint8_t>> clxData;
size_t accumulatedSize = 0;
for (size_t i = 0, j = 0; i < numAnims; ++i) {
if (!monsterData.hasAnim(i))
continue;
const uint32_t begin = result.offsets[j];
const uint32_t end = result.offsets[j + 1];
clxData.emplace_back();
Cl2ToClx(reinterpret_cast<uint8_t *>(&result.data[begin]), end - begin,
PointerOrValue<uint16_t> { monsterData.width }, clxData.back());
result.offsets[j] = accumulatedSize;
accumulatedSize += clxData.back().size();
++j;
}
result.offsets[clxData.size()] = accumulatedSize;
result.data = nullptr;
result.data = std::unique_ptr<std::byte[]>(new std::byte[accumulatedSize]);
for (size_t i = 0; i < clxData.size(); ++i) {
memcpy(&result.data[result.offsets[i]], clxData[i].data(), clxData[i].size());
}
#endif

return result;
}

} // namespace

void InitTRNForUniqueMonster(Monster &monster)
Expand Down Expand Up @@ -3321,7 +3385,7 @@ void InitMonsterSND(CMonster &monsterType)
};

const MonsterData &data = MonstersData[monsterType.type];
std::string_view soundSuffix = data.soundSuffix != nullptr ? data.soundSuffix : data.assetsSuffix;
std::string_view soundSuffix = data.soundPath();

for (int i = 0; i < 4; i++) {
std::string_view prefix = prefixes[i];
Expand All @@ -3336,74 +3400,31 @@ void InitMonsterSND(CMonster &monsterType)
}
}

void InitMonsterGFX(CMonster &monsterType)
void InitMonsterGFX(CMonster &monsterType, MonsterSpritesData &&spritesData)
{
if (HeadlessMode)
return;

const _monster_id mtype = monsterType.type;
const MonsterData &monsterData = MonstersData[mtype];
const size_t numAnims = GetNumAnims(monsterData);
const auto hasAnim = [&monsterData](size_t index) {
return monsterData.frames[index] != 0;
};
constexpr size_t MaxAnims = 6;
std::array<uint32_t, MaxAnims + 1> animOffsets;
if (!HeadlessMode) {
monsterType.animData = MultiFileLoader<MaxAnims> {}(
numAnims,
FileNameWithCharAffixGenerator({ "monsters\\", monsterData.assetsSuffix }, DEVILUTIONX_CL2_EXT, Animletter),
animOffsets.data(),
hasAnim);
}

#ifndef UNPACKED_MPQS
if (!HeadlessMode) {
// Convert CL2 to CLX:
std::vector<std::vector<uint8_t>> clxData;
size_t accumulatedSize = 0;
for (size_t i = 0, j = 0; i < numAnims; ++i) {
if (!hasAnim(i))
continue;
const uint32_t begin = animOffsets[j];
const uint32_t end = animOffsets[j + 1];
clxData.emplace_back();
Cl2ToClx(reinterpret_cast<uint8_t *>(&monsterType.animData[begin]), end - begin,
PointerOrValue<uint16_t> { monsterData.width }, clxData.back());
animOffsets[j] = accumulatedSize;
accumulatedSize += clxData.back().size();
++j;
}
animOffsets[clxData.size()] = accumulatedSize;
monsterType.animData = nullptr;
monsterType.animData = std::unique_ptr<std::byte[]>(new std::byte[accumulatedSize]);
for (size_t i = 0; i < clxData.size(); ++i) {
memcpy(&monsterType.animData[animOffsets[i]], clxData[i].data(), clxData[i].size());
}
}
#endif
if (spritesData.data == nullptr)
spritesData = LoadMonsterSpritesData(monsterData);
monsterType.animData = std::move(spritesData.data);

const size_t numAnims = GetNumAnims(monsterData);
for (size_t i = 0, j = 0; i < numAnims; ++i) {
AnimStruct &anim = monsterType.anims[i];
if (!hasAnim(i)) {
anim.frames = 0;
if (!monsterData.hasAnim(i)) {
monsterType.anims[i].sprites = std::nullopt;
continue;
}
anim.frames = monsterData.frames[i];
anim.rate = monsterData.rate[i];
anim.width = monsterData.width;
if (!HeadlessMode) {
const uint32_t begin = animOffsets[j];
const uint32_t end = animOffsets[j + 1];
auto spritesData = reinterpret_cast<uint8_t *>(&monsterType.animData[begin]);
const uint16_t numLists = GetNumListsFromClxListOrSheetBuffer(spritesData, end - begin);
anim.sprites = ClxSpriteListOrSheet { spritesData, numLists };
}
const uint32_t begin = spritesData.offsets[j];
const uint32_t end = spritesData.offsets[j + 1];
auto spritesData = reinterpret_cast<uint8_t *>(&monsterType.animData[begin]);
const uint16_t numLists = GetNumListsFromClxListOrSheetBuffer(spritesData, end - begin);
monsterType.anims[i].sprites = ClxSpriteListOrSheet { spritesData, numLists };
++j;
}

monsterType.data = &monsterData;

if (HeadlessMode)
return;

if (monsterData.trnFile != nullptr) {
InitMonsterTRN(monsterType);
}
Expand Down Expand Up @@ -3451,6 +3472,39 @@ void InitMonsterGFX(CMonster &monsterType)
GetMissileSpriteData(MissileGraphicID::DiabloApocalypseBoom).LoadGFX();
}

void InitAllMonsterGFX()
{
if (HeadlessMode)
return;

using LevelMonsterTypeIndices = StaticVector<uint8_t, 8>;
std::array<LevelMonsterTypeIndices, enum_size<MonsterSpriteId>::value> monstersBySprite;
for (size_t i = 0; i < LevelMonsterTypeCount; ++i) {
monstersBySprite[static_cast<size_t>(LevelMonsterTypes[i].data().spriteId)].emplace_back(i);
}
uint32_t totalUniqueBytes = 0;
uint32_t totalBytes = 0;
for (const LevelMonsterTypeIndices &monsterTypes : monstersBySprite) {
if (monsterTypes.empty())
continue;
CMonster &firstMonster = LevelMonsterTypes[monsterTypes[0]];
if (firstMonster.animData != nullptr)
continue;
MonsterSpritesData spritesData = LoadMonsterSpritesData(firstMonster.data());
const size_t spritesDataSize = spritesData.offsets[GetNumAnimsWithGraphics(firstMonster.data())];
for (size_t i = 1; i < monsterTypes.size(); ++i) {
MonsterSpritesData spritesDataCopy { std::unique_ptr<std::byte[]> { new std::byte[spritesDataSize] }, spritesData.offsets };
memcpy(spritesDataCopy.data.get(), spritesData.data.get(), spritesDataSize);
InitMonsterGFX(LevelMonsterTypes[monsterTypes[i]], std::move(spritesDataCopy));
}
LogVerbose("Loaded monster graphics: {:15s} {:>4d} KiB x{:d}", firstMonster.data().spritePath(), spritesDataSize / 1024, monsterTypes.size());
totalUniqueBytes += spritesDataSize;
totalBytes += spritesDataSize * monsterTypes.size();
InitMonsterGFX(firstMonster, std::move(spritesData));
}
LogVerbose(" Total monster graphics: {:>4d} KiB {:>4d} KiB", totalUniqueBytes / 1024, totalBytes / 1024);
}

void WeakenNaKrul()
{
if (currlevel != 24 || static_cast<size_t>(UberDiabloMonsterIndex) >= ActiveMonsterCount)
Expand Down Expand Up @@ -3530,6 +3584,8 @@ void InitMonsters()
DoUnVision(trigs[i].position + Displacement { s, t }, 15);
}
}

InitAllMonsterGFX();
}

void SetMapMonsters(const uint16_t *dunData, Point startPosition)
Expand Down Expand Up @@ -4037,6 +4093,7 @@ void FreeMonsters()
{
for (CMonster &monsterType : LevelMonsterTypes) {
monsterType.animData = nullptr;
monsterType.corpseId = 0;
for (AnimStruct &animData : monsterType.anims) {
animData.sprites = std::nullopt;
}
Expand Down Expand Up @@ -4166,7 +4223,7 @@ void SyncMonsterAnim(Monster &monster)
#ifdef _DEBUG
// fix for saves with debug monsters having type originally not on the level
CMonster &monsterType = LevelMonsterTypes[monster.levelType];
if (monsterType.data == nullptr) {
if (monsterType.corpseId == 0) {
InitMonsterGFX(monsterType);
monsterType.corpseId = 1;
}
Expand Down
Loading

0 comments on commit 8d6bf4f

Please sign in to comment.