Enable M2 particle emitters with correct WotLK struct parsing and overflow guards

This commit is contained in:
Kelsi 2026-02-06 20:57:02 -08:00
parent d54aba3950
commit 51be0beea6
2 changed files with 77 additions and 21 deletions

View file

@ -252,12 +252,16 @@ T readValue(const std::vector<uint8_t>& data, uint32_t offset) {
template<typename T> template<typename T>
std::vector<T> readArray(const std::vector<uint8_t>& data, uint32_t offset, uint32_t count) { std::vector<T> readArray(const std::vector<uint8_t>& data, uint32_t offset, uint32_t count) {
std::vector<T> result; std::vector<T> result;
if (count == 0 || offset + count * sizeof(T) > data.size()) { if (count == 0) return result;
return result; // Overflow-safe bounds check: avoid uint32 wrap on count * sizeof(T)
} size_t totalBytes = static_cast<size_t>(count) * sizeof(T);
if (totalBytes / sizeof(T) != count) return result; // multiplication overflowed
if (static_cast<size_t>(offset) + totalBytes > data.size()) return result;
// Sanity cap: refuse allocations > 64MB to prevent garbage counts from OOMing
if (totalBytes > 64 * 1024 * 1024) return result;
result.resize(count); result.resize(count);
std::memcpy(result.data(), &data[offset], count * sizeof(T)); std::memcpy(result.data(), &data[offset], totalBytes);
return result; return result;
} }
@ -652,17 +656,61 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
model.attachmentLookup = readArray<uint16_t>(m2Data, header.ofsAttachmentLookup, header.nAttachmentLookup); model.attachmentLookup = readArray<uint16_t>(m2Data, header.ofsAttachmentLookup, header.nAttachmentLookup);
} }
// Particle emitter parsing disabled. // Parse particle emitters (WotLK M2ParticleOld: 0x1F0 = 496 bytes per emitter)
// The assumed EMITTER_STRUCT_SIZE (476 bytes) is incorrect for WotLK 3.3.5a M2 files. static constexpr uint32_t EMITTER_STRUCT_SIZE = 0x1F0;
// When iterating multiple emitters, each one after the first reads from a misaligned if (header.nParticleEmitters > 0 && header.ofsParticleEmitters > 0 &&
// offset, producing garbage M2TrackDisk headers with huge nTimestamps/nKeys counts. header.nParticleEmitters < 256 &&
// parseAnimTrack then calls readArray which allocates vectors sized by those garbage static_cast<size_t>(header.ofsParticleEmitters) +
// counts — this caused RAM usage to explode from ~1 GB to 130+ GB, consuming all static_cast<size_t>(header.nParticleEmitters) * EMITTER_STRUCT_SIZE <= m2Data.size()) {
// system memory and swap.
// TODO: determine the correct emitter struct size for build 12340 and add overflow // Build sequence flags for parseAnimTrack
// guards to readArray (count * sizeof(T) can wrap uint32_t, bypassing bounds checks). std::vector<uint32_t> emSeqFlags;
(void)header.nParticleEmitters; emSeqFlags.reserve(model.sequences.size());
(void)header.ofsParticleEmitters; for (const auto& seq : model.sequences) {
emSeqFlags.push_back(seq.flags);
}
for (uint32_t ei = 0; ei < header.nParticleEmitters; ei++) {
uint32_t base = header.ofsParticleEmitters + ei * EMITTER_STRUCT_SIZE;
M2ParticleEmitter em;
em.particleId = readValue<int32_t>(m2Data, base + 0x00);
em.flags = readValue<uint32_t>(m2Data, base + 0x04);
em.position.x = readValue<float>(m2Data, base + 0x08);
em.position.y = readValue<float>(m2Data, base + 0x0C);
em.position.z = readValue<float>(m2Data, base + 0x10);
em.bone = readValue<uint16_t>(m2Data, base + 0x14);
em.texture = readValue<uint16_t>(m2Data, base + 0x16);
em.blendingType = readValue<uint8_t>(m2Data, base + 0x28);
em.emitterType = readValue<uint8_t>(m2Data, base + 0x29);
// Parse animated tracks (M2TrackDisk at known offsets)
auto parseTrack = [&](uint32_t off, M2AnimationTrack& track) {
if (base + off + sizeof(M2TrackDisk) <= m2Data.size()) {
M2TrackDisk disk = readValue<M2TrackDisk>(m2Data, base + off);
parseAnimTrack(m2Data, disk, track, TrackType::FLOAT, emSeqFlags);
}
};
parseTrack(0x34, em.emissionSpeed);
parseTrack(0x48, em.speedVariation);
parseTrack(0x5C, em.verticalRange);
parseTrack(0x70, em.horizontalRange);
parseTrack(0x84, em.gravity);
parseTrack(0x98, em.lifespan);
parseTrack(0xB0, em.emissionRate);
parseTrack(0xC8, em.emissionAreaLength);
parseTrack(0xDC, em.emissionAreaWidth);
parseTrack(0xF0, em.deceleration);
// Parse FBlocks (color, alpha, scale)
parseFBlock(m2Data, base + 0x104, em.particleColor, 0);
parseFBlock(m2Data, base + 0x118, em.particleAlpha, 1);
parseFBlock(m2Data, base + 0x12C, em.particleScale, 2);
model.particleEmitters.push_back(std::move(em));
}
core::Logger::getInstance().debug(" Particle emitters: ", model.particleEmitters.size());
}
static int m2LoadLogBudget = 200; static int m2LoadLogBudget = 200;
if (m2LoadLogBudget-- > 0) { if (m2LoadLogBudget-- > 0) {

View file

@ -824,7 +824,15 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
} }
} }
// Particle emitter data copy disabled (parsing disabled for now) // Copy particle emitter data and resolve textures
gpuModel.particleEmitters = model.particleEmitters;
gpuModel.particleTextures.resize(model.particleEmitters.size(), whiteTexture);
for (size_t ei = 0; ei < model.particleEmitters.size(); ei++) {
uint16_t texIdx = model.particleEmitters[ei].texture;
if (texIdx < allTextures.size() && allTextures[texIdx] != 0) {
gpuModel.particleTextures[ei] = allTextures[texIdx];
}
}
// Copy texture transform data for UV animation // Copy texture transform data for UV animation
gpuModel.textureTransforms = model.textureTransforms; gpuModel.textureTransforms = model.textureTransforms;
@ -1261,11 +1269,11 @@ void M2Renderer::update(float deltaTime) {
computeBoneMatrices(model, instance); computeBoneMatrices(model, instance);
// M2 particle emitter update — disabled for now (too expensive with many instances) // M2 particle emitter update
// if (!model.particleEmitters.empty()) { if (!model.particleEmitters.empty()) {
// emitParticles(instance, model, deltaTime); emitParticles(instance, model, deltaTime);
// updateParticles(instance, deltaTime); updateParticles(instance, deltaTime);
// } }
} }
} }