1#!/usr/bin/env python3 2 3import ctypes 4import sys 5import json 6 7def get_header_properties(header): 8 return { 9 'rootNodeOffset': header.rootNodeOffset, 10 'aabb': { 11 'min_x': header.aabb.min_x, 12 'min_y': header.aabb.min_y, 13 'min_z': header.aabb.min_z, 14 'max_x': header.aabb.max_x, 15 'max_y': header.aabb.max_y, 16 'max_z': header.aabb.max_z, 17 }, 18 'instance_flags': header.instance_flags, 19 'copy_dispatch_size': list(header.copy_dispatch_size), 20 'compacted_size': header.compacted_size, 21 'serialization_size': header.serialization_size, 22 'size': header.size, 23 'instance_count': header.instance_count, 24 'self_ptr': header.self_ptr, 25 'padding': f"{len(header.padding)} uint32_t paddings", 26 } 27 28def get_aabb_leaf_properties(node): 29 return { 30 'leaf_desc': { 31 'shaderIndex': node.leaf_desc.shader_index_and_geom_mask & 0xFFFFFF, 32 'geomMask': (node.leaf_desc.shader_index_and_geom_mask >> 24) & 0xFF, 33 'geomIndex': node.leaf_desc.geometry_id_and_flags & 0xFFFFFF, 34 'subType': (node.leaf_desc.geometry_id_and_flags >> 24) & 0xF, 35 'reserved0': (node.leaf_desc.geometry_id_and_flags >> 28) & 0x1, 36 'DisableOpacityCull': (node.leaf_desc.geometry_id_and_flags >> 29) & 0x1, 37 'OpaqueGeometry': (node.leaf_desc.geometry_id_and_flags >> 30) & 0x1, 38 'IgnoreRayMultiplier': (node.leaf_desc.geometry_id_and_flags >> 31) & 0x1 39 }, 40 'DW1': node.DW1, 41 'primIndex': f"{len(node.primIndex)} uint32" 42 } 43 44def get_quad_leaf_properties(node): 45 return { 46 'leaf_desc': { 47 'shaderIndex': node.leaf_desc.shader_index_and_geom_mask & 0xFFFFFF, 48 'geomMask': (node.leaf_desc.shader_index_and_geom_mask >> 24) & 0xFF, 49 'geomIndex': node.leaf_desc.geometry_id_and_flags & 0xFFFFFF, 50 'subType': (node.leaf_desc.geometry_id_and_flags >> 24) & 0xF, 51 'reserved0': (node.leaf_desc.geometry_id_and_flags >> 28) & 0x1, 52 'DisableOpacityCull': (node.leaf_desc.geometry_id_and_flags >> 29) & 0x1, 53 'OpaqueGeometry': (node.leaf_desc.geometry_id_and_flags >> 30) & 0x1, 54 'IgnoreRayMultiplier': (node.leaf_desc.geometry_id_and_flags >> 31) & 0x1 55 }, 56 'prim_index0': node.prim_index0, 57 'prim_index1_and_flags':{ 58 'primIndex1Delta': node.prim_index1_and_flags & 0xFFFF, 59 'j0': (node.prim_index1_and_flags >> 16) & 0x3, 60 'j1': (node.prim_index1_and_flags >> 18) & 0x3, 61 'j2': (node.prim_index1_and_flags >> 20) & 0x3, 62 'last': (node.prim_index1_and_flags >> 22) & 0x1, 63 'pad': (node.prim_index1_and_flags >> 23) & 0x1FF 64 }, 65 'v': [[node.v[i][j] for j in range(3)] for i in range(4)] 66 } 67 68def get_internal_node_properties(node): 69 # Calculate the actual coordinates, just for visualizing and debugging 70 actual_coords = [] 71 for i in range(6): 72 # Turns out the formula is like: x = lower.x + pow(2,exp_x) * 0.xi 73 xi_lower = node.lower_x[i] / 256.0 # Convert mantissa to fractional value 74 xi_upper = node.upper_x[i] / 256.0 # Convert mantissa to fractional value 75 yi_lower = node.lower_y[i] / 256.0 # Convert mantissa to fractional value 76 yi_upper = node.upper_y[i] / 256.0 # Convert mantissa to fractional value 77 zi_lower = node.lower_z[i] / 256.0 # Convert mantissa to fractional value 78 zi_upper = node.upper_z[i] / 256.0 # Convert mantissa to fractional value 79 80 x_lower = node.lower[0] + (2 ** node.exp_x) * xi_lower 81 x_upper = node.lower[0] + (2 ** node.exp_x) * xi_upper 82 y_lower = node.lower[1] + (2 ** node.exp_y) * yi_lower 83 y_upper = node.lower[1] + (2 ** node.exp_y) * yi_upper 84 z_lower = node.lower[2] + (2 ** node.exp_z) * zi_lower 85 z_upper = node.lower[2] + (2 ** node.exp_z) * zi_upper 86 87 actual_coords.append({ 88 'x_lower': x_lower, 89 'x_upper': x_upper, 90 'y_lower': y_lower, 91 'y_upper': y_upper, 92 'z_lower': z_lower, 93 'z_upper': z_upper 94 }) 95 96 return { 97 'lower': list(node.lower), 98 'child_offset': node.child_offset, 99 'node_type': { 100 'nodeType': node.node_type & 0xF, 101 'subType': (node.node_type >> 4) & 0xF 102 }, 103 'reserved': node.reserved, 104 'exp_x': node.exp_x, 105 'exp_y': node.exp_y, 106 'exp_z': node.exp_z, 107 'node_mask': node.node_mask, 108 'child_data': [{ 109 'blockIncr': node.child_data[i].blockIncr_and_startPrim & 0x3, 110 'startPrim': (node.child_data[i].blockIncr_and_startPrim >> 2) & 0xf 111 } for i in range(6)], 112 'lower_x': list(node.lower_x), 113 'upper_x': list(node.upper_x), 114 'lower_y': list(node.lower_y), 115 'upper_y': list(node.upper_y), 116 'lower_z': list(node.lower_z), 117 'upper_z': list(node.upper_z), 118 'actual_coords': actual_coords 119 } 120 121def get_instance_leaf_properties(node): 122 return { 123 'part0': { 124 'shaderIndex': node.part0.shader_index_and_geom_mask & 0xFFFFFF, 125 'geomMask': (node.part0.shader_index_and_geom_mask >> 24) & 0xFF, 126 'instanceContribution': node.part0.instance_contribution_and_geom_flags & 0xFFFFFF, 127 'pad0': (node.part0.instance_contribution_and_geom_flags >> 24) & 0x1F, 128 'DisableOpacityCull': (node.part0.instance_contribution_and_geom_flags >> 29) & 0x1, 129 'OpaqueGeometry': (node.part0.instance_contribution_and_geom_flags >> 30) & 0x1, 130 'pad1': (node.part0.instance_contribution_and_geom_flags >> 31) & 0x1, 131 'startNodePtr': node.part0.start_node_ptr_and_inst_flags & 0xFFFFFFFFFFFF, 132 'instFlags': (node.part0.start_node_ptr_and_inst_flags >> 48) & 0xFF, 133 'ComparisonMode': (node.part0.start_node_ptr_and_inst_flags >> 56) & 0x1, 134 'ComparisonValue': (node.part0.start_node_ptr_and_inst_flags >> 57) & 0x7F, 135 'world2obj_vx': [node.part0.world2obj_vx_x, node.part0.world2obj_vx_y, node.part0.world2obj_vx_z], 136 'world2obj_vy': [node.part0.world2obj_vy_x, node.part0.world2obj_vy_y, node.part0.world2obj_vy_z], 137 'world2obj_vz': [node.part0.world2obj_vz_x, node.part0.world2obj_vz_y, node.part0.world2obj_vz_z], 138 'obj2world_p': [node.part0.obj2world_p_x, node.part0.obj2world_p_y, node.part0.obj2world_p_z] 139 }, 140 'part1': { 141 'bvh_ptr': node.part1.bvh_ptr, 142 'instance_id': node.part1.instance_id, 143 'instance_index': node.part1.instance_index, 144 'obj2world_vx': [node.part1.obj2world_vx_x, node.part1.obj2world_vx_y, node.part1.obj2world_vx_z], 145 'obj2world_vy': [node.part1.obj2world_vy_x, node.part1.obj2world_vy_y, node.part1.obj2world_vy_z], 146 'obj2world_vz': [node.part1.obj2world_vz_x, node.part1.obj2world_vz_y, node.part1.obj2world_vz_z], 147 'world2obj_p': [node.part1.world2obj_p_x, node.part1.world2obj_p_y, node.part1.world2obj_p_z] 148 } 149 } 150 151class NodeType: 152 NODE_TYPE_MIXED = 0x0 153 NODE_TYPE_INTERNAL = 0x0 154 NODE_TYPE_INSTANCE = 0x1 155 NODE_TYPE_QUAD128_STOC = 0x2 156 NODE_TYPE_PROCEDURAL = 0x3 157 NODE_TYPE_QUAD = 0x4 158 NODE_TYPE_QUAD128 = 0x5 159 NODE_TYPE_MESHLET = 0x6 160 NODE_TYPE_INVALID = 0x7 161 162class VkAabb(ctypes.Structure): 163 _fields_ = ( 164 ('min_x', ctypes.c_float), 165 ('min_y', ctypes.c_float), 166 ('min_z', ctypes.c_float), 167 ('max_x', ctypes.c_float), 168 ('max_y', ctypes.c_float), 169 ('max_z', ctypes.c_float), 170 ) 171 172class AnvAccelStructHeader(ctypes.Structure): 173 _fields_ = ( 174 ('rootNodeOffset', ctypes.c_uint64), 175 ('aabb', VkAabb), 176 ('instance_flags', ctypes.c_uint32), 177 ('copy_dispatch_size', ctypes.c_uint32 * 3), 178 ('compacted_size', ctypes.c_uint64), 179 ('serialization_size', ctypes.c_uint64), 180 ('size', ctypes.c_uint64), 181 ('instance_count', ctypes.c_uint64), 182 ('self_ptr', ctypes.c_uint64), 183 ('padding', ctypes.c_uint32 * 42), 184 ) 185 186class ChildData(ctypes.Structure): 187 _fields_ = ( 188 ('blockIncr_and_startPrim', ctypes.c_uint8), # Assuming child_data has startPrim field 189 ) 190 191class AnvInternalNode(ctypes.Structure): 192 _fields_ = ( 193 ('lower', ctypes.c_float * 3), 194 ('child_offset', ctypes.c_uint32), 195 ('node_type', ctypes.c_uint8), 196 ('reserved', ctypes.c_uint8), 197 ('exp_x', ctypes.c_int8), 198 ('exp_y', ctypes.c_int8), 199 ('exp_z', ctypes.c_int8), 200 ('node_mask', ctypes.c_uint8), 201 ('child_data', ChildData * 6), 202 ('lower_x', ctypes.c_uint8 * 6), 203 ('upper_x', ctypes.c_uint8 * 6), 204 ('lower_y', ctypes.c_uint8 * 6), 205 ('upper_y', ctypes.c_uint8 * 6), 206 ('lower_z', ctypes.c_uint8 * 6), 207 ('upper_z', ctypes.c_uint8 * 6), 208 ) 209 210class AnvPrimLeafDesc(ctypes.Structure): 211 _fields_ = ( 212 ('shader_index_and_geom_mask', ctypes.c_uint32), 213 ('geometry_id_and_flags', ctypes.c_uint32), 214 ) 215 216class AnvQuadLeafNode(ctypes.Structure): 217 _fields_ = ( 218 ('leaf_desc', AnvPrimLeafDesc), 219 ('prim_index0', ctypes.c_uint32), 220 ('prim_index1_and_flags', ctypes.c_uint32), 221 ('v', (ctypes.c_float * 3) * 4), 222 ) 223 224class AnvProceduralLeafNode(ctypes.Structure): 225 _fields_ = ( 226 ('leaf_desc', AnvPrimLeafDesc), 227 ('DW1', ctypes.c_uint32), 228 ('primIndex', ctypes.c_uint32 * 13), 229 ) 230 231class InstanceLeafPart0(ctypes.Structure): 232 _fields_ = ( 233 ('shader_index_and_geom_mask', ctypes.c_uint32), 234 ('instance_contribution_and_geom_flags', ctypes.c_uint32), 235 ('start_node_ptr_and_inst_flags', ctypes.c_uint64), 236 ('world2obj_vx_x', ctypes.c_float), 237 ('world2obj_vx_y', ctypes.c_float), 238 ('world2obj_vx_z', ctypes.c_float), 239 ('world2obj_vy_x', ctypes.c_float), 240 ('world2obj_vy_y', ctypes.c_float), 241 ('world2obj_vy_z', ctypes.c_float), 242 ('world2obj_vz_x', ctypes.c_float), 243 ('world2obj_vz_y', ctypes.c_float), 244 ('world2obj_vz_z', ctypes.c_float), 245 ('obj2world_p_x', ctypes.c_float), 246 ('obj2world_p_y', ctypes.c_float), 247 ('obj2world_p_z', ctypes.c_float), 248 ) 249 250class InstanceLeafPart1(ctypes.Structure): 251 _fields_ = ( 252 ('bvh_ptr', ctypes.c_uint64), 253 ('instance_id', ctypes.c_uint32), 254 ('instance_index', ctypes.c_uint32), 255 ('obj2world_vx_x', ctypes.c_float), 256 ('obj2world_vx_y', ctypes.c_float), 257 ('obj2world_vx_z', ctypes.c_float), 258 ('obj2world_vy_x', ctypes.c_float), 259 ('obj2world_vy_y', ctypes.c_float), 260 ('obj2world_vy_z', ctypes.c_float), 261 ('obj2world_vz_x', ctypes.c_float), 262 ('obj2world_vz_y', ctypes.c_float), 263 ('obj2world_vz_z', ctypes.c_float), 264 ('world2obj_p_x', ctypes.c_float), 265 ('world2obj_p_y', ctypes.c_float), 266 ('world2obj_p_z', ctypes.c_float), 267 ) 268 269class AnvInstanceLeaf(ctypes.Structure): 270 _fields_ = ( 271 ('part0', InstanceLeafPart0), 272 ('part1', InstanceLeafPart1), 273 ) 274 275class BVHInterpreter: 276 def __init__(self, data): 277 self.data = data 278 self.nodes = [] 279 self.relationships = {} 280 self.node_counter = 0 281 282 def interpret_structure(self, offset, structure): 283 size = ctypes.sizeof(structure) 284 if(offset + size > len(self.data)): 285 raise ValueError("Not enought data to interpret this structure.") 286 buffer = self.data[offset:offset + size] 287 return structure.from_buffer_copy(buffer) 288 289 def parse_bvh(self): 290 offset = 0 291 # Interpret the header 292 header = self.interpret_structure(offset, AnvAccelStructHeader) 293 offset += header.rootNodeOffset 294 295 # Interpret the rootNode 296 self.dfs_interpret_node(offset, AnvInternalNode) 297 298 output = { 299 'header': get_header_properties(header), 300 'nodes': self.nodes, 301 'relationships': self.relationships 302 } 303 return output 304 305 def determine_child_structure(self, child_node_type): 306 if child_node_type == NodeType.NODE_TYPE_MIXED: 307 return AnvInternalNode 308 elif child_node_type == NodeType.NODE_TYPE_INSTANCE: 309 return AnvInstanceLeaf 310 elif child_node_type == NodeType.NODE_TYPE_QUAD: 311 return AnvQuadLeafNode 312 elif child_node_type == NodeType.NODE_TYPE_PROCEDURAL: 313 return AnvProceduralLeafNode 314 else: 315 raise ValueError(f"Unknown node type: {child_node_type}") 316 317 def dfs_interpret_node(self, offset, structure): 318 node = self.interpret_structure(offset, structure) 319 node_id = self.node_counter; 320 self.node_counter += 1 321 322 if structure == AnvInternalNode: 323 node_type_str = "AnvInternalNode" 324 node_properties = get_internal_node_properties(node) 325 elif structure == AnvInstanceLeaf: 326 node_type_str = "AnvInstanceLeaf" 327 node_properties = get_instance_leaf_properties(node) 328 elif structure == AnvQuadLeafNode: 329 node_type_str = "AnvQuadLeafNode" 330 node_properties = get_quad_leaf_properties(node) 331 elif structure == AnvProceduralLeafNode: 332 node_type_str = "AnvProceduralLeafNode" 333 node_properties = get_aabb_leaf_properties(node) 334 else: 335 raise ValueError(f"Unknown structure type: {structure}") 336 337 self.nodes.append({ 338 'id': node_id, 339 'type': node_type_str, 340 'properties': node_properties 341 }) 342 343 self.relationships[node_id] = [] 344 345 if node_type_str == "AnvInternalNode": 346 # DFS its children 347 children_offset_start = offset + node.child_offset * 64 # this node's position + child_offset 348 isFatLeaf = True if node.node_type != NodeType.NODE_TYPE_MIXED else False 349 added_blocks = 0 350 for i in range(6): 351 blockIncr = node.child_data[i].blockIncr_and_startPrim & 0x3 352 child_is_valid = not (node.lower_x[i] & 0x80) or (node.upper_x[i] & 0x80) 353 if(not child_is_valid): 354 continue 355 356 # now determine the children's type 357 child_node_type = node.node_type if isFatLeaf else ((node.child_data[i].blockIncr_and_startPrim >> 2) & 0xf) 358 359 # find where my child is 360 child_offset = children_offset_start + 64 * added_blocks 361 added_blocks += blockIncr 362 363 child_node_id = self.dfs_interpret_node(child_offset, self.determine_child_structure(child_node_type)) 364 self.relationships[node_id].append(child_node_id) 365 366 return node_id 367 368 369def main(): 370 with open(sys.argv[1], 'rb') as file1: 371 data = file1.read() 372 interpreter = BVHInterpreter(data) 373 json_output = interpreter.parse_bvh() 374 with open("bvh_dump.json", 'w') as f: 375 json.dump(json_output, f, indent=4) 376 377 378if __name__=="__main__": 379 main() 380