Skip to content

Commit b6ae386

Browse files
authored
fix: attempt both 16-bit and 32-bit instructions (#10)
1 parent d3c6d58 commit b6ae386

1 file changed

Lines changed: 77 additions & 16 deletions

File tree

  • src/hpsdecode/schemas

src/hpsdecode/schemas/cc.py

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
__all__ = ["CCSchemaParser"]
66

77
import typing as t
8+
from enum import Enum
89

910
import numpy as np
1011

@@ -19,6 +20,13 @@
1920
import numpy.typing as npt
2021

2122

23+
class IndexMode(Enum):
24+
"""Enum for vertex index mode."""
25+
26+
MODE_16BIT = 16
27+
MODE_32BIT = 32
28+
29+
2230
class CCSchemaParser(BaseSchemaParser):
2331
"""Parser for the 'CC' HPS compression schema."""
2432

@@ -38,7 +46,7 @@ def parse(self, context: ParseContext) -> ParseResult:
3846
:return: The parsing result containing the decoded mesh and commands.
3947
"""
4048
vertices, vertex_commands = self.parse_vertices(context.vertex_data)
41-
faces, face_commands = self.parse_faces(context.face_data)
49+
faces, face_commands = self.parse_faces(context.face_data, context.face_count, len(vertices))
4250

4351
uv = self._parse_texture_coords(context.texture_coords_data, vertices.shape[0], faces)
4452
texture_images = [image for image in context.texture_images if isinstance(image, bytes)]
@@ -64,20 +72,43 @@ def parse(self, context: ParseContext) -> ParseResult:
6472
face_commands=face_commands,
6573
)
6674

67-
def parse_faces(self, data: bytes) -> tuple[npt.NDArray[np.integer], list[hpc.AnyFaceCommand]]:
75+
def parse_faces(
76+
self,
77+
data: bytes,
78+
face_count: int,
79+
vertex_count: int,
80+
) -> tuple[npt.NDArray[np.integer], list[hpc.AnyFaceCommand]]:
6881
"""Parse face data from bytes.
6982
7083
:param data: The raw byte data containing face data.
84+
:param face_count: Expected number of faces (used for validation).
85+
:param vertex_count: Total number of vertices (used for validation).
7186
:return: An array of face indices (M, 3) and the face commands.
7287
"""
73-
self._clear()
74-
75-
commands = self._parse_commands(data)
76-
for command in commands:
77-
self._process_command(command)
78-
79-
faces = np.array(self._faces, dtype=np.int32)
80-
return faces, commands
88+
errors: list[HPSParseError] = []
89+
for mode in (IndexMode.MODE_16BIT, IndexMode.MODE_32BIT):
90+
try:
91+
self._clear()
92+
93+
commands = self._parse_commands(data, mode)
94+
for command in commands:
95+
self._process_command(command, vertex_count)
96+
97+
faces = np.array(self._faces, dtype=np.int32)
98+
if len(faces) != face_count:
99+
raise HPSParseError(
100+
f"Face count mismatch in {mode.name} mode: expected {face_count}, got {len(faces)}"
101+
)
102+
103+
return faces, commands
104+
except HPSParseError as e:
105+
errors.append(e)
106+
continue
107+
108+
raise HPSParseError(
109+
"Failed to parse face data with both 16-bit and 32-bit index modes.\n"
110+
"Errors encountered:\n" + "\n".join(f" - {e}" for e in errors)
111+
)
81112

82113
def parse_vertices(self, data: bytes) -> tuple[npt.NDArray[np.floating], list[hpc.AnyVertexCommand]]:
83114
"""Parse vertex data from bytes.
@@ -206,10 +237,11 @@ def _next_global_vertex(self) -> int:
206237
self._global_vertex_ptr += 1
207238
return v
208239

209-
def _parse_commands(self, data: bytes) -> list[hpc.AnyFaceCommand]:
240+
def _parse_commands(self, data: bytes, mode: IndexMode) -> list[hpc.AnyFaceCommand]:
210241
"""Parse face commands from the binary data.
211242
212243
:param data: The raw byte data containing face commands.
244+
:param mode: The index mode (16-bit or 32-bit).
213245
:return: A list of parsed face commands.
214246
"""
215247
reader = BinaryReader(data)
@@ -225,16 +257,17 @@ def _parse_commands(self, data: bytes) -> list[hpc.AnyFaceCommand]:
225257
raise HPSParseError("Upper 4 bits of face command byte must be zero", offset=reader.position - 1)
226258

227259
opcode = command_byte & 0x0F
228-
command = self._parse_single_command(reader, opcode)
260+
command = self._parse_single_command(reader, opcode, mode)
229261
commands.append(command)
230262

231263
return commands
232264

233-
def _parse_single_command(self, reader: BinaryReader, opcode: int) -> hpc.AnyFaceCommand:
265+
def _parse_single_command(self, reader: BinaryReader, opcode: int, mode: IndexMode) -> hpc.AnyFaceCommand:
234266
"""Parse a single command given its opcode.
235267
236268
:param reader: The binary reader positioned after the opcode byte.
237269
:param opcode: The command opcode.
270+
:param mode: The index mode (16-bit or 32-bit).
238271
:return: The parsed command.
239272
:raises HPSParseError: If the opcode is unknown.
240273
"""
@@ -250,6 +283,14 @@ def _parse_single_command(self, reader: BinaryReader, opcode: int) -> hpc.AnyFac
250283
case hpc.FaceCommandType.RESTART:
251284
return hpc.Restart()
252285
case hpc.FaceCommandType.RESTART_16:
286+
if mode == IndexMode.MODE_32BIT:
287+
# 16-bit opcode but 32-bit payload (╯°□°)╯︵ ┻━┻
288+
return hpc.Restart16(
289+
v0=reader.read_uint32(),
290+
v1=reader.read_uint32(),
291+
v2=reader.read_uint32(),
292+
)
293+
253294
return hpc.Restart16(
254295
v0=reader.read_uint16(),
255296
v1=reader.read_uint16(),
@@ -262,8 +303,11 @@ def _parse_single_command(self, reader: BinaryReader, opcode: int) -> hpc.AnyFac
262303
v2=reader.read_uint32(),
263304
)
264305
case hpc.FaceCommandType.ABSOLUTE_16:
265-
# Why can the Absolute16 command have 32-bit values? (╯°□°)╯︵ ┻━┻
266-
return hpc.Absolute16(v=reader.read_uint32())
306+
if mode == IndexMode.MODE_32BIT:
307+
# 16-bit opcode but 32-bit payload (╯°□°)╯︵ ┻━┻
308+
return hpc.Absolute16(v=reader.read_uint32())
309+
310+
return hpc.Absolute16(v=reader.read_uint16())
267311
case hpc.FaceCommandType.ABSOLUTE_32:
268312
return hpc.Absolute32(v=reader.read_uint32())
269313
case hpc.FaceCommandType.REMOVE:
@@ -273,10 +317,11 @@ def _parse_single_command(self, reader: BinaryReader, opcode: int) -> hpc.AnyFac
273317
case _:
274318
raise HPSParseError(f"Unknown face command opcode: {opcode}", offset=reader.position)
275319

276-
def _process_command(self, command: hpc.AnyFaceCommand) -> None:
320+
def _process_command(self, command: hpc.AnyFaceCommand, vertex_count: int | None = None) -> None:
277321
"""Process a single face command and update internal state.
278322
279323
:param command: The command to process.
324+
:param vertex_count: The total number of vertices for bounds checking.
280325
"""
281326
match command.op:
282327
case hpc.FaceCommandType.VERTEX_LIST:
@@ -295,8 +340,10 @@ def _process_command(self, command: hpc.AnyFaceCommand) -> None:
295340
v2 = self._next_global_vertex()
296341
self._create_restart_face(v0, v1, v2)
297342
case hpc.FaceCommandType.RESTART_16 | hpc.FaceCommandType.RESTART_32:
343+
self._validate_indices(command.v0, command.v1, command.v2, vertex_count=vertex_count)
298344
self._create_restart_face(command.v0, command.v1, command.v2)
299345
case hpc.FaceCommandType.ABSOLUTE_16 | hpc.FaceCommandType.ABSOLUTE_32:
346+
self._validate_indices(command.v, vertex_count=vertex_count)
300347
self._extend_current_edge(command.v)
301348
self._increase_edge_pointer(2)
302349
case hpc.FaceCommandType.REMOVE:
@@ -425,3 +472,17 @@ def _remove_current_edge(self) -> None:
425472
self._current_edge_idx = curr_idx % len(self._edge_list)
426473
else:
427474
self._current_edge_idx = 0
475+
476+
def _validate_indices(self, *indices: int, vertex_count: int | None) -> None:
477+
"""Validate vertex indices are within expected range.
478+
479+
:param indices: The vertex indices to validate.
480+
:param vertex_count: The total number of vertices for bounds checking.
481+
:raises HPSParseError: If index is out of bounds.
482+
"""
483+
if vertex_count is None:
484+
return
485+
486+
for v in indices:
487+
if v < 0 or v >= vertex_count:
488+
raise HPSParseError(f"Vertex index {v} out of bounds (0 to {vertex_count - 1})")

0 commit comments

Comments
 (0)