Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
LegacyMeshData.cpp
Go to the documentation of this file.
1#include "LegacyMeshData.hpp"
2
3#include <algorithm>
4#include <bit>
5#include <limits>
6#include <unordered_map>
7#include <unordered_set>
8#include <utility>
9
10namespace monopoly::data
11{
12 namespace
13 {
14 constexpr std::uint32_t Flag = 0x8000'0000U;
15 constexpr std::uint32_t EndOfChain = 0xFFFF'FFFFU;
16
17 MeshDataError error(MeshDataErrorCode code, std::size_t offset,
18 std::string detail)
19 {
20 return { code, offset, std::move(detail), std::nullopt };
21 }
22
23 bool fits(std::span<const std::byte> bytes, std::size_t offset,
24 std::size_t count, std::size_t stride = 1) noexcept
25 {
26 return offset <= bytes.size() &&
27 count <= (bytes.size() - offset) / stride;
28 }
29
30 std::uint32_t u32(std::span<const std::byte> bytes,
31 std::size_t offset) noexcept
32 {
33 std::uint32_t result = 0;
34 for (unsigned i = 0; i < 4; ++i)
35 result |= std::to_integer<std::uint32_t>(bytes[offset + i])
36 << (i * 8U);
37 return result;
38 }
39
40 std::uint16_t u16(std::span<const std::byte> bytes,
41 std::size_t offset) noexcept
42 {
43 return static_cast<std::uint16_t>(
44 std::to_integer<std::uint16_t>(bytes[offset]) |
45 (std::to_integer<std::uint16_t>(bytes[offset + 1]) << 8U));
46 }
47
48 std::uint16_t texturePage8(std::uint16_t x, std::uint16_t y) noexcept
49 {
50 // Source/PC3D/Libgpu.h getTPage(1, 0, x, y).
51 return static_cast<std::uint16_t>(
52 (1U << 7U) | ((y & 0x100U) >> 4U) |
53 ((x & 0x3FFU) >> 6U) | ((y & 0x200U) << 2U));
54 }
55
56 std::expected<std::size_t, MeshDataError> wordOffset(
57 std::span<const std::byte> bytes, std::uint32_t value,
58 std::size_t fieldOffset, std::size_t minimumSize)
59 {
60 // Check before multiplying, including on a 32-bit target.
61 if (value > bytes.size() / 4 ||
62 !fits(bytes, static_cast<std::size_t>(value) * 4, minimumSize))
63 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
64 fieldOffset, "HMD word offset leaves the owning payload"));
65 return static_cast<std::size_t>(value) * 4;
66 }
67
68 HmdShortVector shortVector(std::span<const std::byte> bytes,
69 std::size_t offset) noexcept
70 {
71 const auto a = u32(bytes, offset);
72 const auto b = u32(bytes, offset + 4);
73 return {
74 std::bit_cast<std::int16_t>(static_cast<std::uint16_t>(a)),
75 std::bit_cast<std::int16_t>(static_cast<std::uint16_t>(a >> 16)),
76 std::bit_cast<std::int16_t>(static_cast<std::uint16_t>(b)),
77 std::bit_cast<std::int16_t>(static_cast<std::uint16_t>(b >> 16))
78 };
79 }
80 }
81
82 std::string_view meshDataErrorCodeName(MeshDataErrorCode code) noexcept
83 {
84 switch (code)
85 {
86 case MeshDataErrorCode::None: return "None";
87 case MeshDataErrorCode::MissingPayload: return "MissingPayload";
88 case MeshDataErrorCode::HeaderTruncated: return "HeaderTruncated";
89 case MeshDataErrorCode::AlreadyMapped: return "AlreadyMapped";
90 case MeshDataErrorCode::RangeOutOfBounds: return "RangeOutOfBounds";
91 case MeshDataErrorCode::InvalidPrimitiveHeader: return "InvalidPrimitiveHeader";
92 case MeshDataErrorCode::InvalidSectionSize: return "InvalidSectionSize";
93 case MeshDataErrorCode::PrimitiveCycle: return "PrimitiveCycle";
94 case MeshDataErrorCode::RecordLimitExceeded: return "RecordLimitExceeded";
95 case MeshDataErrorCode::IndexOutOfRange: return "IndexOutOfRange";
96 case MeshDataErrorCode::UnsupportedSection: return "UnsupportedSection";
97 case MeshDataErrorCode::ExpectedOffset: return "ExpectedOffset";
98 case MeshDataErrorCode::DataLoadFailed: return "DataLoadFailed";
99 case MeshDataErrorCode::TypeMismatch: return "TypeMismatch";
100 }
101 return "Unknown";
102 }
103
104 std::expected<LegacyMeshData, MeshDataError> LegacyMeshData::parse(
105 SharedDataBytes owner, MeshParseLimits limits)
106 {
107 if (!owner)
108 return std::unexpected(error(MeshDataErrorCode::MissingPayload, 0,
109 "HMD requires an owning DAT payload"));
110 const std::span<const std::byte> data(*owner);
111 if (!fits(data, 0, 16))
112 return std::unexpected(error(MeshDataErrorCode::HeaderTruncated, 0,
113 "HMD header requires four little-endian DWORDs"));
114 if (u32(data, 4) != 0)
115 return std::unexpected(error(MeshDataErrorCode::AlreadyMapped, 4,
116 "Relocated process pointers are not an HMD disk payload"));
117
118 LegacyMeshData result;
119 result.owner_ = std::move(owner);
120 // Neither historical loader checks a magic/version constant; retain it.
121 result.versionId_ = u32(data, 0);
122 const auto blockCount = u32(data, 12);
123 if (!fits(data, 16, blockCount, 4))
124 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
125 12, "HMD block table is truncated"));
126 if (blockCount > limits.maximumPrimitives)
127 return std::unexpected(error(MeshDataErrorCode::RecordLimitExceeded,
128 12, "HMD block table exceeds configured record budget"));
129
130 const auto headerSection = wordOffset(data, u32(data, 8), 8, 4);
131 if (!headerSection) return std::unexpected(headerSection.error());
132 const auto headerCount = u32(data, *headerSection);
133 std::size_t cursor = *headerSection + 4;
134 if (!fits(data, cursor, headerCount, 4))
135 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
136 *headerSection, "HMD primitive header table is truncated"));
137 if (headerCount > limits.maximumHeaderFields)
138 return std::unexpected(error(MeshDataErrorCode::RecordLimitExceeded,
139 *headerSection, "HMD header count exceeds configured budget"));
140
141 // NewMesh.cpp:129-153: count, then [fieldCount, field...] headers.
142 std::unordered_map<std::size_t, std::size_t> headerIndices;
143 std::size_t totalFields = 0;
144 for (std::uint32_t i = 0; i < headerCount; ++i)
145 {
146 if (!fits(data, cursor, 4))
147 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
148 cursor, "Missing HMD primitive header field count"));
151 const auto count = u32(data, cursor);
152 cursor += 4;
153 if (!fits(data, cursor, count, 4))
154 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
155 header.offset, "HMD primitive header fields are truncated"));
157 return std::unexpected(error(MeshDataErrorCode::RecordLimitExceeded,
158 header.offset, "HMD header fields exceed configured budget"));
160 for (std::uint32_t j = 0; j < count; ++j, cursor += 4)
161 {
162 const auto value = u32(data, cursor);
163 if ((value & Flag) != 0)
164 {
165 const auto resolved = wordOffset(data, value & ~Flag, cursor, 1);
166 if (!resolved) return std::unexpected(resolved.error());
167 }
168 header.fields.push_back({ value });
169 }
170 headerIndices.emplace(header.offset, result.headers_.size());
171 result.headers_.push_back(std::move(header));
172 }
173
174 std::unordered_map<std::size_t, std::size_t> primitiveIndices;
175 std::size_t totalSections = 0;
176 for (std::uint32_t block = 0; block < blockCount; ++block)
177 {
178 const auto fieldOffset = 16 + static_cast<std::size_t>(block) * 4;
179 const auto rootWord = u32(data, fieldOffset);
180 if (rootWord == 0)
181 {
182 result.blockRoots_.push_back(std::nullopt);
183 continue;
184 }
185 const auto root = wordOffset(data, rootWord, fieldOffset, 12);
186 if (!root) return std::unexpected(root.error());
187 result.blockRoots_.push_back(*root);
188 std::optional<std::size_t> next = *root;
189 std::unordered_set<std::size_t> activeChain;
190 while (next)
191 {
192 const auto position = *next;
193 if (!activeChain.insert(position).second)
194 return std::unexpected(error(MeshDataErrorCode::PrimitiveCycle,
195 position, "Cycle in HMD primitive next chain"));
196 // A completed chain shared by another block needs no reparse.
197 if (primitiveIndices.contains(position)) break;
198 if (result.primitives_.size() >= limits.maximumPrimitives)
199 return std::unexpected(error(MeshDataErrorCode::RecordLimitExceeded,
200 position, "HMD primitives exceed configured budget"));
201
203 primitive.offset = position;
204 const auto nextWord = u32(data, position);
205 if (nextWord != EndOfChain)
206 {
207 const auto resolved = wordOffset(data, nextWord, position, 12);
208 if (!resolved) return std::unexpected(resolved.error());
209 primitive.nextOffset = *resolved;
210 }
211 const auto headerOffset = wordOffset(data,
212 u32(data, position + 4), position + 4, 4);
213 if (!headerOffset) return std::unexpected(headerOffset.error());
214 const auto found = headerIndices.find(*headerOffset);
215 if (found == headerIndices.end())
217 position + 4, "Primitive does not reference a declared header"));
218 primitive.headerIndex = found->second;
219 const auto countWord = u32(data, position + 8);
220 primitive.processRequired = (countWord & Flag) != 0;
221 const auto sectionCount = countWord & ~Flag;
222 cursor = position + 12;
223 if (!fits(data, cursor, sectionCount, 8))
224 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
225 position + 8, "HMD data section count exceeds available bytes"));
227 return std::unexpected(error(MeshDataErrorCode::RecordLimitExceeded,
228 position + 8, "HMD sections exceed configured budget"));
230 for (std::uint32_t i = 0; i < sectionCount; ++i)
231 {
232 if (!fits(data, cursor, 8))
233 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
234 cursor, "HMD data section header is truncated"));
235 const auto countSize = u32(data, cursor + 4);
236 const auto words = (countSize & 0xFFFFU) + 1U;
237 if (words < 2)
238 return std::unexpected(error(MeshDataErrorCode::InvalidSectionSize,
239 cursor + 4, "HMD section cannot be shorter than its header"));
240 if (!fits(data, cursor, words, 4))
241 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
242 cursor + 4, "HMD data section extends beyond payload"));
243 primitive.sections.push_back({ cursor, u32(data, cursor), words,
244 static_cast<std::uint16_t>((countSize >> 16) & 0x7FFFU),
245 (countSize & Flag) != 0 });
246 cursor += static_cast<std::size_t>(words) * 4;
247 }
248 next = primitive.nextOffset;
249 primitiveIndices.emplace(position, result.primitives_.size());
250 result.primitives_.push_back(std::move(primitive));
251 }
252 }
253 return result;
254 }
255
256 std::uint32_t LegacyMeshData::versionId() const noexcept { return versionId_; }
257 std::span<const std::optional<std::size_t>> LegacyMeshData::blockRoots() const noexcept
258 { return blockRoots_; }
259 std::span<const HmdPrimitiveHeader> LegacyMeshData::headers() const noexcept
260 { return headers_; }
261 std::span<const HmdPrimitive> LegacyMeshData::primitives() const noexcept
262 { return primitives_; }
263 std::span<const std::byte> LegacyMeshData::bytes() const noexcept
264 { return owner_ ? std::span<const std::byte>(*owner_) : std::span<const std::byte>{}; }
265
266 std::expected<std::span<const std::byte>, MeshDataError>
268 std::size_t sectionIndex) const
269 {
270 if (primitiveIndex >= primitives_.size() ||
271 sectionIndex >= primitives_[primitiveIndex].sections.size())
272 return std::unexpected(error(MeshDataErrorCode::IndexOutOfRange, 0,
273 "HMD primitive/section index is out of range"));
274 const auto& section = primitives_[primitiveIndex].sections[sectionIndex];
275 return bytes().subspan(section.offset,
276 static_cast<std::size_t>(section.sizeWords) * 4);
277 }
278
279 std::expected<HmdTriangle, MeshDataError> LegacyMeshData::triangle(
280 std::size_t primitiveIndex, std::size_t sectionIndex,
281 std::size_t triangleIndex) const
282 {
284 if (!rawSection) return std::unexpected(rawSection.error());
285 const auto& primitive = primitives_[primitiveIndex];
286 const auto& section = primitive.sections[sectionIndex];
287 const auto type = section.type;
288 // Category 0, shape 1, normal-bearing triangles. Other categories,
289 // no-normal variants and preset packets remain bounded opaque sections.
290 if (((type >> 24) & 0xFU) != 0 || ((type >> 3) & 7U) != 1 ||
291 (type & (0x40U | 0x100U)) != 0)
292 return std::unexpected(error(MeshDataErrorCode::UnsupportedSection,
293 section.offset, "Section is not a supported normal-bearing triangle"));
294 if (triangleIndex >= section.elementCount)
295 return std::unexpected(error(MeshDataErrorCode::IndexOutOfRange,
296 section.offset, "Triangle index exceeds section polygon count"));
297 if (rawSection->size() < 12)
298 return std::unexpected(error(MeshDataErrorCode::InvalidSectionSize,
299 section.offset, "Triangle section requires a polygon word offset"));
300 const auto& header = headers_[primitive.headerIndex];
301 if (header.fields.size() < 3)
303 header.offset, "Triangle header needs polygon, vertex and normal fields"));
304 std::array<std::size_t, 3> bases{};
305 for (std::size_t i = 0; i < bases.size(); ++i)
306 {
307 if (!header.fields[i].isOffset())
308 return std::unexpected(error(MeshDataErrorCode::ExpectedOffset,
309 header.offset + 4 + i * 4, "Triangle field is a literal, not an offset"));
310 bases[i] = static_cast<std::size_t>(header.fields[i].rawValue & ~Flag) * 4;
311 }
312 const bool textured = (type & 1U) != 0;
313 const bool separateColours = (type & 2U) != 0;
314 const bool gouraud = (type & 4U) != 0;
315 const bool tiled = (type & 0x200U) != 0;
316 const std::size_t colours = textured && !separateColours ? 0 :
317 (separateColours ? 3 : 1);
318 const std::size_t recordWords = (tiled ? 1U : 0U) + colours +
319 (textured ? 3U : 0U) + (gouraud ? 3U : 2U);
320 const auto polygonWord = u32(*rawSection, 8);
321 const auto data = bytes();
322 // Offset is relative to the polygon field, unlike all header pointers.
323 if (polygonWord > (data.size() - bases[0]) / 4)
324 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
325 section.offset + 8, "Polygon offset leaves owning HMD payload"));
326 const auto polygonStart = bases[0] + static_cast<std::size_t>(polygonWord) * 4;
327 if (!fits(data, polygonStart, section.elementCount, recordWords * 4))
328 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
329 polygonStart, "Triangle count extends beyond owning HMD payload"));
331 HmdTriangle result;
332 if (tiled) cursor += 4;
333 if (colours == 0) result.colours.fill(0x00FF'FFFFU);
334 else
335 {
336 for (std::size_t i = 0; i < colours; ++i, cursor += 4)
337 result.colours[i] = u32(data, cursor);
338 if (colours == 1) result.colours.fill(result.colours[0]);
339 }
340 if (textured)
341 {
342 for (std::size_t i = 0; i < 3; ++i, cursor += 4)
343 {
344 const auto value = u32(data, cursor);
345 result.texturePoints[i] = { static_cast<std::uint8_t>(value),
346 static_cast<std::uint8_t>(value >> 8) };
347 if (i == 1) result.texturePage = static_cast<std::uint16_t>(value >> 16);
348 }
349 }
350 if (gouraud)
351 {
352 for (std::size_t i = 0; i < 3; ++i, cursor += 4)
353 {
354 const auto value = u32(data, cursor);
355 result.vertexIndices[i] = static_cast<std::uint16_t>(value >> 16);
356 result.normalIndices[i] = static_cast<std::uint16_t>(value);
357 }
358 }
359 else
360 {
361 const auto first = u32(data, cursor);
362 const auto second = u32(data, cursor + 4);
363 result.vertexIndices = { static_cast<std::uint16_t>(first >> 16),
364 static_cast<std::uint16_t>(second), static_cast<std::uint16_t>(second >> 16) };
365 result.normalIndices.fill(static_cast<std::uint16_t>(first));
366 }
367 for (std::size_t i = 0; i < 3; ++i)
368 {
369 const auto vertexIndex = static_cast<std::size_t>(result.vertexIndices[i]);
370 const auto normalIndex = static_cast<std::size_t>(result.normalIndices[i]);
371 if (!fits(data, bases[1], vertexIndex + 1, 8) ||
372 !fits(data, bases[2], normalIndex + 1, 8))
373 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
374 polygonStart, "Triangle references an out-of-payload SVECTOR"));
375 result.vertices[i] = shortVector(data, bases[1] + vertexIndex * 8);
376 result.normals[i] = shortVector(data, bases[2] + normalIndex * 8);
377 }
378 return result;
379 }
380
381 std::expected<std::vector<HmdMimePose>, MeshDataError>
383 {
384 const auto data = bytes();
385 std::vector<HmdMimeDiffBlock> vertexBlocks;
386 std::vector<HmdMimeDiffBlock> normalBlocks;
387 std::size_t totalBlocks{};
388 std::size_t totalVectors{};
389
390 std::unordered_map<std::size_t, std::size_t> primitiveByOffset;
391 for (std::size_t index = 0; index < primitives_.size(); ++index)
392 primitiveByOffset.emplace(primitives_[index].offset, index);
393
394 auto relativeWordOffset = [&](std::size_t base, std::uint32_t words,
395 std::size_t fieldOffset, std::size_t minimumSize)
396 -> std::expected<std::size_t, MeshDataError>
397 {
398 if (base > data.size() || words > (data.size() - base) / 4U)
399 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
400 fieldOffset, "MIMe word offset leaves the diff section"));
401 const auto offset = base + static_cast<std::size_t>(words) * 4U;
402 if (!fits(data, offset, minimumSize))
403 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
404 fieldOffset, "MIMe target is truncated"));
405 return offset;
406 };
407
408 auto decodeSection = [&](const HmdPrimitive& primitive,
409 const HmdDataSection& section, bool normal)
410 -> std::expected<void, MeshDataError>
411 {
412 if (!primitive.processRequired || !section.scanRequired)
413 return {};
414 const auto& header = headers_[primitive.headerIndex];
415 if (header.fields.size() < 4)
417 header.offset, "MIMe header needs a diff-block base field"));
418 if (!header.fields[3].isOffset())
419 return std::unexpected(error(MeshDataErrorCode::ExpectedOffset,
420 header.offset + 16U,
421 "MIMe diff-block base must be a word offset"));
422 const auto blockBase = wordOffset(data,
423 header.fields[3].rawValue & ~Flag, header.offset + 16U, 8U);
424 if (!blockBase)
425 return std::unexpected(blockBase.error());
426 if (section.sizeWords < 2U + section.elementCount)
427 return std::unexpected(error(MeshDataErrorCode::InvalidSectionSize,
428 section.offset, "MIMe record offsets exceed section size"));
429
430 auto& destination = normal ? normalBlocks : vertexBlocks;
431 for (std::size_t item = 0; item < section.elementCount; ++item)
432 {
433 const auto itemField = section.offset + (2U + item) * 4U;
434 const auto current = relativeWordOffset(*blockBase,
435 u32(data, itemField), itemField, 8U);
436 if (!current)
437 return std::unexpected(current.error());
438 const auto counts = u32(data, *current);
439 const auto diffCount = normal
440 ? static_cast<std::size_t>(counts & 0xFFFFU)
441 : static_cast<std::size_t>(counts >> 16U);
442 if (!fits(data, *current + 8U, diffCount, 4U))
443 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
444 *current, "MIMe current-block diff offsets are truncated"));
445
446 for (std::size_t diff = 0; diff < diffCount; ++diff)
447 {
448 if (totalBlocks >= limits.maximumDiffBlocks)
449 return std::unexpected(error(
451 "MIMe diff-block budget exceeded"));
452 const auto diffField = *current + (2U + diff) * 4U;
454 u32(data, diffField), diffField, 8U);
455 if (!blockOffset)
456 return std::unexpected(blockOffset.error());
457 const auto vectorCount = static_cast<std::size_t>(
458 u32(data, *blockOffset + 4U) >> 16U);
459 if (totalVectors > limits.maximumVectors ||
461 return std::unexpected(error(
463 *blockOffset + 4U, "MIMe SVECTOR budget exceeded"));
464 if (!fits(data, *blockOffset + 8U, vectorCount, 8U))
465 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
466 *blockOffset, "MIMe diff SVECTOR run is truncated"));
467
470 decoded.startIndex = u32(data, *blockOffset);
471 decoded.diffs.reserve(vectorCount);
472 for (std::size_t vector = 0; vector < vectorCount; ++vector)
473 decoded.diffs.push_back(shortVector(data,
474 *blockOffset + 8U + vector * 8U));
476 ++totalBlocks;
477 destination.push_back(std::move(decoded));
478 }
479 }
480 return {};
481 };
482
483 std::unordered_set<std::size_t> processedPrimitives;
484 for (const auto& root : blockRoots_)
485 {
486 if (!root)
487 continue;
488 std::vector<std::size_t> chain;
489 std::optional<std::size_t> current = *root;
490 while (current && !processedPrimitives.contains(*current))
491 {
492 const auto found = primitiveByOffset.find(*current);
493 if (found == primitiveByOffset.end())
494 return std::unexpected(error(
496 "MIMe chain references an unknown primitive"));
497 chain.push_back(found->second);
498 current = primitives_[found->second].nextOffset;
499 }
500 for (auto iterator = chain.rbegin(); iterator != chain.rend(); ++iterator)
501 {
502 const auto& primitive = primitives_[*iterator];
503 if (!processedPrimitives.insert(primitive.offset).second)
504 continue;
505 for (const auto& section : primitive.sections)
506 {
507 const auto type = section.type & 0xFF7F'FFFFU;
508 if (type == 0x0401'0020U)
509 {
510 auto decoded = decodeSection(primitive, section, false);
511 if (!decoded)
512 return std::unexpected(decoded.error());
513 }
514 else if (type == 0x0401'0021U)
515 {
516 auto decoded = decodeSection(primitive, section, true);
517 if (!decoded)
518 return std::unexpected(decoded.error());
519 }
520 }
521 }
522 }
523
524 const auto poseCount = std::max(vertexBlocks.size(), normalBlocks.size());
525 std::vector<HmdMimePose> poses(poseCount);
526 for (std::size_t index = 0; index < poseCount; ++index)
527 {
528 if (index < vertexBlocks.size())
529 poses[index].vertex = std::move(vertexBlocks[index]);
530 if (index < normalBlocks.size())
531 poses[index].normal = std::move(normalBlocks[index]);
532 }
533 return poses;
534 }
535
536 std::expected<std::vector<HmdTextureImage>, MeshDataError>
538 {
539 const auto data = bytes();
540 std::vector<HmdTextureImage> result;
541 for (const auto& primitive : primitives_)
542 {
543 const auto& header = headers_[primitive.headerIndex];
544 for (const auto& section : primitive.sections)
545 {
546 // HMDData.h HMDDT_IMAGE=2. The PC loader only calls
547 // ProcessHMDImage for GsUIMG1 (category 2 + CLUT bit 0).
548 if (((section.type >> 24U) & 0xFU) != 2U ||
549 (section.type & 1U) == 0U)
550 continue;
551 if (header.fields.size() < 2)
553 header.offset, "HMD image header needs IMAGE TOP and CLUT TOP fields"));
554 if (!header.fields[0].isOffset() || !header.fields[1].isOffset())
555 return std::unexpected(error(MeshDataErrorCode::ExpectedOffset,
556 header.offset + 4, "HMD image header bases must be word offsets"));
557 const auto imageBase = static_cast<std::size_t>(
558 header.fields[0].rawValue & ~Flag) * 4U;
559 const auto clutBase = static_cast<std::size_t>(
560 header.fields[1].rawValue & ~Flag) * 4U;
561
562 // File Formats 2-95: each WITHCLUT entry is six DWORDs after
563 // the shared type/count pair: image x/y, w/h, image_idx,
564 // CLUT x/y, w/h, clut_idx. hmdload.cpp advances by six.
565 const std::size_t requiredWords = 2U +
566 static_cast<std::size_t>(section.elementCount) * 6U;
567 if (section.sizeWords < requiredWords)
568 return std::unexpected(error(MeshDataErrorCode::InvalidSectionSize,
569 section.offset, "GsUIMG1 records exceed HMD image section size"));
570 const auto raw = data.subspan(section.offset,
571 static_cast<std::size_t>(section.sizeWords) * 4U);
572
573 for (std::size_t i = 0; i < section.elementCount; ++i)
574 {
575 const auto record = 2U + i * 6U;
576 const auto xy = u32(raw, record * 4U);
577 const auto wh = u32(raw, (record + 1U) * 4U);
578 const auto imageIndex = u32(raw, (record + 2U) * 4U);
579 const auto clutIndex = u32(raw, (record + 5U) * 4U);
580 const auto rawX = static_cast<std::uint16_t>(xy);
581 const auto rawY = static_cast<std::uint16_t>(xy >> 16U);
582 const auto widthWords = static_cast<std::uint16_t>(wh);
583 const auto height16 = static_cast<std::uint16_t>(wh >> 16U);
584 const std::uint32_t width =
585 static_cast<std::uint32_t>(widthWords) * 2U;
586 const std::uint32_t height = height16;
587 if (width == 0U || height == 0U ||
588 width > std::numeric_limits<std::size_t>::max() / height)
589 return std::unexpected(error(MeshDataErrorCode::InvalidSectionSize,
590 section.offset + record * 4U,
591 "HMD 8-bit texture has invalid dimensions"));
592 const auto pixelCount =
593 static_cast<std::size_t>(width) * height;
594 if (pixelCount > std::numeric_limits<std::size_t>::max() / 4U)
595 return std::unexpected(error(MeshDataErrorCode::InvalidSectionSize,
596 section.offset + record * 4U,
597 "HMD texture RGBA size overflows host range"));
598 if (imageBase > data.size() ||
599 imageIndex > (data.size() - imageBase) / 4U)
600 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
601 section.offset + (record + 2U) * 4U,
602 "HMD image index leaves IMAGE TOP section"));
603 if (clutBase > data.size() ||
604 clutIndex > (data.size() - clutBase) / 4U)
605 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
606 section.offset + (record + 5U) * 4U,
607 "HMD CLUT index leaves CLUT TOP section"));
608 const auto imageOffset = imageBase +
609 static_cast<std::size_t>(imageIndex) * 4U;
610 const auto clutOffset = clutBase +
611 static_cast<std::size_t>(clutIndex) * 4U;
613 !fits(data, clutOffset, 256U, 2U))
614 return std::unexpected(error(MeshDataErrorCode::RangeOutOfBounds,
615 section.offset, "HMD image or 256-entry CLUT is truncated"));
616
618 image.texturePage = texturePage8(rawX, rawY);
619 image.rawX = rawX;
620 image.rawY = rawY;
621 image.logicalX = static_cast<std::int32_t>((rawX & 0x3FU) << 1U);
622 image.logicalY = static_cast<std::int32_t>(rawY & 0xFFU);
623 image.width = width;
624 image.height = height;
625
626 // TextureHasBeenLoaded compares page/bank/raw X/Y and
627 // suppresses duplicates before allocating a Surface.
628 const auto duplicate = std::find_if(result.begin(), result.end(),
629 [&](const HmdTextureImage& existing)
630 {
631 return existing.texturePage == image.texturePage &&
632 existing.rawX == rawX && existing.rawY == rawY;
633 });
634 if (duplicate != result.end()) continue;
635
636 image.rgba.resize(pixelCount * 4U);
637 for (std::size_t pixel = 0; pixel < pixelCount; ++pixel)
638 {
639 const auto paletteIndex =
640 std::to_integer<std::uint8_t>(data[imageOffset + pixel]);
641 const auto colour = u16(data, clutOffset +
642 static_cast<std::size_t>(paletteIndex) * 2U);
643 auto expand5 = [](std::uint16_t value) -> std::uint8_t
644 {
645 return static_cast<std::uint8_t>(
646 (static_cast<std::uint32_t>(value) * 255U) / 31U);
647 };
648 image.rgba[pixel * 4U] = expand5(colour & 0x1FU);
649 image.rgba[pixel * 4U + 1U] = expand5((colour >> 5U) & 0x1FU);
650 image.rgba[pixel * 4U + 2U] = expand5((colour >> 10U) & 0x1FU);
651 image.rgba[pixel * 4U + 3U] = 255U;
652 }
653 result.push_back(std::move(image));
654 }
655 }
656 }
657 return result;
658 }
659
660 std::expected<LegacyMeshData, MeshDataError> openLegacyMeshData(
661 const DataSource& source, DataId id, MeshParseLimits limits)
662 {
663 const auto metadata = source.metadata(id);
664 if (!metadata)
665 return std::unexpected(MeshDataError{ MeshDataErrorCode::DataLoadFailed,
666 0, "Cannot inspect HMD DAT item", metadata.error() });
667 if (metadata->type != LegacyDataType::Hmd)
668 return std::unexpected(error(MeshDataErrorCode::TypeMismatch,
669 0, "DAT item is not an unmapped HMD payload"));
670 const auto payload = source.load(id);
671 if (!payload)
672 return std::unexpected(MeshDataError{ MeshDataErrorCode::DataLoadFailed,
673 0, "Cannot load HMD DAT item", payload.error() });
674 return LegacyMeshData::parse(*payload, limits);
675 }
676}
std::size_t count
std::vector< std::uint8_t > data
std::size_t offset
std::size_t root
const char * header
LegacyBitmapMetadata metadata
LegacyBitmapRGBA8 image
std::vector< std::byte > bytes
std::shared_ptr< const data::HmdTextureImage > source
std::uint32_t height
std::uint32_t width
unsigned index
data::DataTag first
int colour
std::string error
std::uint16_t value
Definition Timers.cpp:16
std::span< const HmdPrimitive > primitives() const noexcept
std::expected< std::vector< HmdTextureImage >, MeshDataError > textureImages() const
std::expected< std::span< const std::byte >, MeshDataError > sectionBytes(std::size_t primitiveIndex, std::size_t sectionIndex) const
std::span< const std::optional< std::size_t > > blockRoots() const noexcept
std::span< const HmdPrimitiveHeader > headers() const noexcept
std::expected< std::vector< HmdMimePose >, MeshDataError > mimePoses(HmdMimeLimits limits={}) const
std::uint32_t versionId() const noexcept
std::expected< HmdTriangle, MeshDataError > triangle(std::size_t primitiveIndex, std::size_t sectionIndex, std::size_t triangleIndex) const
std::span< const std::byte > bytes() const noexcept
static std::expected< LegacyMeshData, MeshDataError > parse(SharedDataBytes bytes, MeshParseLimits limits={})
std::expected< LegacyMeshData, MeshDataError > openLegacyMeshData(const DataSource &source, DataId id, MeshParseLimits limits)
std::uint32_t DataId
Definition DataBanks.hpp:10
std::shared_ptr< const DataBytes > SharedDataBytes
std::string_view meshDataErrorCodeName(MeshDataErrorCode code) noexcept
std::array< HmdTexturePoint, 3 > texturePoints
std::array< std::uint16_t, 3 > normalIndices
std::array< HmdShortVector, 3 > normals
std::array< std::uint32_t, 3 > colours
std::array< HmdShortVector, 3 > vertices
std::array< std::uint16_t, 3 > vertexIndices