diff --git a/port/linux/game/powerup_render_bounds.c b/port/linux/game/powerup_render_bounds.c new file mode 100644 index 00000000..ab971816 --- /dev/null +++ b/port/linux/game/powerup_render_bounds.c @@ -0,0 +1,186 @@ +/* +POWERUP_RENDER_BOUNDS.C + +The overshield's and active camouflage's authored render spheres (the +object's render_bounding_radius, which only culling reads: objects.h, +object_get_render_bounding_sphere) are smaller than their meshes, so near the +edges of the screen they were culled while still in view. When a map's tags +load (scenario_tags_load), each powerup of those two kinds with a rigid +one-node model and no animations gets a radius that holds every vertex of +every geometry and part of its model, around its bounding offset. The radius +only ever grows; the bounding radius physics, pickups and the game state use +is left as it is, so nothing played or sent between machines changes. +*/ + +#include "cseries.h" +#include "math/real_math.h" +#include "cache/cache_files.h" +#include "items/equipment_definitions.h" +#include "models/model_definitions.h" +#include "rasterizer/rasterizer.h" +#include "rasterizer/rasterizer_geometry.h" +#include "rasterizer/rasterizer_model_types.h" +#include "tag_files/tag_groups.h" + +#include +#include + +/* ---------- constants */ + +enum +{ + _model_geometry_part_stripped_bit = 0, + _model_geometry_part_local_nodes_bit, +}; + +/* ---------- structures */ + +/* this build's model geometry and part, as models.c and rasterizer.c each +define them for themselves (no header declares them) */ +struct model_geometry +{ + byte reserved[0x24]; + struct tag_block parts; +}; + +struct model_geometry_part +{ + unsigned long flags; + short shader_index; + char previous_part_index; + char next_part_index; + short centroid_primary_node_index; + short centroid_secondary_node_index; + real centroid_primary_node_weight; + real centroid_secondary_node_weight; + real_point3d centroid; + struct tag_block uncompressed_vertices; + struct tag_block compressed_vertices; + struct tag_block triangles; + struct triangle_buffer triangle_buffer; + struct vertex_buffer vertex_buffer; +}; + +typedef char verify_model_geometry_part_size[sizeof(struct model_geometry_part) == 0x68 ? 1 : -1]; + +/* ---------- private code */ + +/* the radius around center that holds every vertex of a rigid model's +geometries; FALSE if the model is not one, or a vertex cannot be read */ +static boolean model_rigid_render_radius( + struct model const *model, + real_point3d const *center, + real *radius) +{ + struct model_node const *root; + real maximum_squared = 0.f; + long vertex_count = 0; + long geometry_index; + + if (model->nodes.count != 1 || !model->nodes.address || + model->geometries.count <= 0 || model->geometries.count > MAXIMUM_GEOMETRIES_PER_MODEL || + !model->geometries.address) + { + return FALSE; + } + + /* every level of detail and permutation, in the root node's space */ + root = TAG_BLOCK_GET_ELEMENT(&model->nodes, 0, struct model_node); + for (geometry_index = 0; geometry_index < model->geometries.count; geometry_index++) + { + struct model_geometry const *geometry = TAG_BLOCK_GET_ELEMENT(&model->geometries, geometry_index, struct model_geometry); + long part_index; + + if (geometry->parts.count < 0 || geometry->parts.count > MAXIMUM_PARTS_PER_MODEL_GEOMETRY || + (geometry->parts.count && !geometry->parts.address)) + { + return FALSE; + } + + for (part_index = 0; part_index < geometry->parts.count; part_index++) + { + struct model_geometry_part const *part = TAG_BLOCK_GET_ELEMENT(&geometry->parts, part_index, struct model_geometry_part); + struct vertex_buffer const *buffer = &part->vertex_buffer; + byte *vertices = NULL; + boolean valid = TRUE; + long stride; + long vertex_index; + + if (TEST_FLAG(part->flags, _model_geometry_part_stripped_bit)) + { + continue; + } + if (TEST_FLAG(part->flags, _model_geometry_part_local_nodes_bit) || !buffer->hardware_format || buffer->offset || + buffer->count <= 0 || buffer->count > MAXIMUM_VERTICES_PER_MODEL_GEOMETRY_PART || + (buffer->type != _rasterizer_vertex_type_model_compressed && buffer->type != _rasterizer_vertex_type_model_uncompressed)) + { + return FALSE; + } + + stride = rasterizer_geometry_get_vertex_size(buffer->type); + IDirect3DVertexBuffer8_Lock(buffer->hardware_format, 0, 0, &vertices, D3DLOCK_READONLY); + valid = vertices != NULL; + for (vertex_index = 0; valid && vertex_index < buffer->count; vertex_index++) + { + real_point3d point; + real distance_squared; + + matrix4x3_transform_point(&root->runtime_default_inverse_matrix, + (real_point3d const *)(vertices + vertex_index * stride), &point); + distance_squared = distance_squared3d(&point, center); + /* (not a number, or infinite) */ + if (!(distance_squared >= 0.f && distance_squared < FLT_MAX)) + { + valid = FALSE; + } + else + { + maximum_squared = MAX(maximum_squared, distance_squared); + } + } + IDirect3DVertexBuffer8_Unlock(buffer->hardware_format); + if (!valid) + { + return FALSE; + } + vertex_count += buffer->count; + } + } + + if (!vertex_count) + { + return FALSE; + } + + /* a little past the farthest vertex, so its rounding leaves it inside */ + *radius = square_root(maximum_squared) * 1.0001f; + + return TRUE; +} + +/* ---------- public code */ + +void powerup_render_bounds_tags_loaded( + void) +{ + struct tag_iterator iterator; + long equipment_index; + + tag_iterator_new(&iterator, EQUIPMENT_DEFINITION_TAG); + while ((equipment_index = tag_iterator_next(&iterator)) != NONE) + { + struct equipment_definition *equipment = equipment_definition_get(equipment_index); + real radius; + + if ((equipment->equipment.powerup_type == _equipment_powerup_overshield || + equipment->equipment.powerup_type == _equipment_powerup_active_camouflage) && + equipment->object.model.index != NONE && equipment->object.animation_graph.index == NONE && + model_rigid_render_radius(model_definition_get(equipment->object.model.index), &equipment->object.bounding_offset, &radius) && + radius > equipment->object.render_bounding_radius) + { + equipment->object.render_bounding_radius = radius; + } + } + + return; +} diff --git a/source/cache/cache_files.c b/source/cache/cache_files.c index 91c488c6..f04bfb50 100644 --- a/source/cache/cache_files.c +++ b/source/cache/cache_files.c @@ -134,7 +134,6 @@ symbols in this file: #include "interface/ui_widget.h" #include "scenario/scenario_definitions.h" #include "sound/sound_manager.h" -#include "models/models.h" #include "tag_schema.h" #include "custom_edition_cache.h" /* port: port/linux/game/custom_edition_cache.c */ #include "cache_file_formats.h" /* port: CUSTOM_EDITION_TAG_CACHE_BYTES */ @@ -1271,9 +1270,13 @@ long scenario_tags_load( pal_tags_loaded(cache_file_globals.header.build); } - /* The two powerups' authored render spheres can be smaller than - * their rigid meshes. Derive their bounds while tags are writable. */ - models_fix_powerup_render_bounds(); + /* port: the powerups' render spheres, grown to hold their meshes + (port/linux/game/powerup_render_bounds.c) */ + { + extern void powerup_render_bounds_tags_loaded(void); + + powerup_render_bounds_tags_loaded(); + } /* port: the menus' tags, added to the map's (port/linux/game/menu_tags.c) */ { extern void menu_tags_loaded(char const *map_name); diff --git a/source/models/models.c b/source/models/models.c index 3e7ba1e5..f087bf87 100644 --- a/source/models/models.c +++ b/source/models/models.c @@ -83,10 +83,6 @@ symbols in this file: #include "shaders/shaders.h" #include "rasterizer/rasterizer_console_vars.h" #include "rasterizer/rasterizer_model_types.h" -#include "cache/cache_files.h" -#include "items/equipment_definitions.h" -#include -#include /* ---------- constants */ @@ -462,82 +458,6 @@ static void render_model_parts( /* ---------- public code */ -static boolean model_rigid_render_radius( - struct model const *model, - real_point3d const *center, - real *radius) -{ - struct model_node const *root; - real maximum_squared = 0.0f; - long geometry_index, vertex_count = 0; - if (model->nodes.count != 1 || !model->nodes.address || - model->geometries.count <= 0 || model->geometries.count > MAXIMUM_GEOMETRIES_PER_MODEL || - !model->geometries.address) return FALSE; - root = TAG_BLOCK_GET_ELEMENT(&model->nodes, 0, struct model_node); - /* Include every LOD/permutation. A one-node model has a fixed mesh in - * root-node space; skeletal animation needs a different bounds policy. */ - for (geometry_index = 0; geometry_index < model->geometries.count; ++geometry_index) - { - struct model_geometry const *geometry = TAG_BLOCK_GET_ELEMENT(&model->geometries, geometry_index, struct model_geometry); - long part_index; - if (geometry->parts.count < 0 || geometry->parts.count > MAXIMUM_PARTS_PER_MODEL_GEOMETRY || - (geometry->parts.count && !geometry->parts.address)) return FALSE; - for (part_index = 0; part_index < geometry->parts.count; ++part_index) - { - struct model_geometry_part const *part = TAG_BLOCK_GET_ELEMENT(&geometry->parts, part_index, struct model_geometry_part); - struct vertex_buffer const *buffer = &part->vertex_buffer; - byte *vertices = NULL; - long stride, i; - boolean valid = TRUE; - if (TEST_FLAG(part->flags, _model_geometry_part_stripped_bit)) continue; - if (TEST_FLAG(part->flags, _model_geometry_part_local_nodes_bit) || !buffer->hardware_format || buffer->offset || - buffer->count <= 0 || buffer->count > MAXIMUM_VERTICES_PER_MODEL_GEOMETRY_PART || - (buffer->type != _rasterizer_vertex_type_model_compressed && buffer->type != _rasterizer_vertex_type_model_uncompressed)) return FALSE; - stride = rasterizer_geometry_get_vertex_size(buffer->type); - IDirect3DVertexBuffer8_Lock(buffer->hardware_format, 0, 0, &vertices, D3DLOCK_READONLY); - if (!vertices) valid = FALSE; - for (i = 0; valid && i < buffer->count; ++i) - { - real_point3d point; - real squared; - matrix4x3_transform_point(&root->runtime_default_inverse_matrix, - (real_point3d const *)(vertices + i * stride), &point); - squared = distance_squared3d(&point, center); - if (!(squared >= 0.0f && squared < FLT_MAX)) valid = FALSE; - else maximum_squared = MAX(maximum_squared, squared); - } - IDirect3DVertexBuffer8_Unlock(buffer->hardware_format); - if (!valid) return FALSE; - vertex_count += buffer->count; - } - } - if (!vertex_count) return FALSE; - /* Round outward so the extremal vertex stays inside the sphere. */ - *radius = nextafterf(sqrtf(maximum_squared), FLT_MAX); - return TRUE; -} - -void models_fix_powerup_render_bounds(void) -{ - struct tag_iterator iterator; - long index; - tag_iterator_new(&iterator, EQUIPMENT_DEFINITION_TAG); - while ((index = tag_iterator_next(&iterator)) != NONE) - { - struct equipment_definition *equipment = equipment_definition_get(index); - real radius; - if ((equipment->equipment.powerup_type == _equipment_powerup_overshield || - equipment->equipment.powerup_type == _equipment_powerup_active_camouflage) && - equipment->object.model.index != NONE && equipment->object.animation_graph.index == NONE && - model_rigid_render_radius(model_definition_get(equipment->object.model.index), &equipment->object.bounding_offset, &radius) && - radius > equipment->object.render_bounding_radius) - { - /* Derived tag data only: leave physics, pickup radius and saves intact. */ - equipment->object.render_bounding_radius = radius; - } - } -} - void model_interpolate_node_orientations( struct model const *model, struct real_orientation *original_node_orientations, diff --git a/source/models/models.h b/source/models/models.h index d8445784..44c49396 100644 --- a/source/models/models.h +++ b/source/models/models.h @@ -27,8 +27,6 @@ union real_vector3d; /* ---------- prototypes/MODELS.C */ -void models_fix_powerup_render_bounds(void); - void model_interpolate_node_orientations( struct model const *model, struct real_orientation *original_node_orientations, diff --git a/source/objects/objects.h b/source/objects/objects.h index 00e76414..ee691bc0 100644 --- a/source/objects/objects.h +++ b/source/objects/objects.h @@ -529,10 +529,12 @@ __inline void object_get_render_bounding_sphere( *center = object->object.bounding_sphere_center; *radius = object_definition_get(object->definition_index)->object.render_bounding_radius; - /* Growing an object must grow its culling sphere. Preserve the existing - * conservative sphere for scales at or below one. */ - if (object->object.scale > 1.0f) + /* port: (grown with an object scaled up, as its bounding sphere is in + object_compute_node_matrices; one scaled down keeps the larger sphere) */ + if (object->object.scale > 1.f) + { *radius *= object->object.scale; + } return; } diff --git a/tools/test_powerup_bounds.py b/tools/test_powerup_bounds.py deleted file mode 100644 index 616b1d42..00000000 --- a/tools/test_powerup_bounds.py +++ /dev/null @@ -1,248 +0,0 @@ -"""Test production powerup bounds and frustum culling, without a game/profile. - ---map additionally measures both actual Xbox powerup meshes. Assets stay local. -""" -import argparse -import re -import os -from pathlib import Path -import struct -import subprocess -import sys -import tempfile -import zlib -ROOT=Path(__file__).resolve().parent.parent - -def block(source, marker): - start = source.index(marker) - end = source.index('{', start) + 1 - depth = 1 - while depth: - depth += (source[end] == '{') - (source[end] == '}') - end += 1 - return source[start:end] - -PRELUDE=r''' -#define _CRT_SECURE_NO_WARNINGS -#include -#include -#include -#include -#include -#include -typedef unsigned char byte,boolean; -typedef unsigned short word; -typedef float real; -typedef union {struct {real x,y,z;};struct {real i,j,k;};real n[3];} real_point3d,real_vector3d; -typedef struct {real i,j;} real_vector2d; -typedef struct {real red,green,blue;} real_rgb_color; -typedef struct {real i,j,k,w;} real_quaternion; -typedef struct real_matrix4x3 {real scale;real_vector3d forward,left,up;real_point3d position;} real_matrix4x3; -typedef struct {real_vector3d n;real d;} real_plane3d; -typedef struct {real x0,x1,y0,y1,z0,z1;} real_rectangle3d; -struct render_frustum {real_rectangle3d world_bounds;real_plane3d world_planes[6];}; -struct tag_block {long count;void *address,*definition;}; -struct tag_reference {unsigned long group_tag;char *name;long name_length,index;}; -struct tag_iterator {long absolute_index;}; -#define TRUE 1 -#define FALSE 0 -#define NONE -1 -#define FLAG(b) (1u<<(b)) -#define TEST_FLAG(f,b) (((f)&FLAG(b))!=0) -#define MAX(a,b) ((a)>(b)?(a):(b)) -#define TAG_STRING_LENGTH 31 -#define UNSIGNED_SHORT_MAX 65535 -#define TAG_BLOCK_GET_ELEMENT(b,i,t) ((t*)((byte*)(b)->address+(i)*sizeof(t))) -#define D3DLOCK_READONLY 128 -#define CHECK(c) do{if(!(c)){fprintf(stderr,"FAIL %d: %s\n",__LINE__,#c);exit(1);}}while(0) -#define match_assert(file,line,c) CHECK(c) -#define OBJECTS_H_FILE "test" -struct test_buffer {byte *data;}; -static void IDirect3DVertexBuffer8_Lock(void *b,int offset,int size,byte **data,int flags){*data=b&&((struct test_buffer*)b)->data?((struct test_buffer*)b)->data+offset:NULL;} -static void IDirect3DVertexBuffer8_Unlock(void *b){} -static long rasterizer_geometry_get_vertex_size(short type){return type==5?32:68;} -''' -HARNESS=r''' -static struct model models[8]; -static struct equipment_definition equipment[8]; -static long equipment_count; -#define equipment_definition_get(i) (equipment+(i)) -#define object_definition_get(i) ((struct object_definition*)(equipment+(i))) -#define model_definition_get(i) (models+(i)) -static void tag_iterator_new(struct tag_iterator *i,long group){i->absolute_index=0;} -static long tag_iterator_next(struct tag_iterator *i){return i->absolute_indexabsolute_index++:NONE;} -struct object_datum {long definition_index;struct {real_point3d bounding_sphere_center;real scale;} object;}; -static struct object_datum object; -static struct object_datum *object_get(long i){return &object;} -''' -TESTS=r''' -static struct model_node node; -static struct model_geometry geometries[2]; -static struct model_geometry_part parts[2]; -static float vertices[2][3][17]; -static struct test_buffer buffers[2]; -static void initialize(void){ - memset(equipment,0,sizeof(equipment));memset(models,0,sizeof(models));memset(&node,0,sizeof(node)); - memset(geometries,0,sizeof(geometries));memset(parts,0,sizeof(parts));memset(vertices,0,sizeof(vertices)); - node.runtime_default_inverse_matrix=(real_matrix4x3){.scale=1,.forward={.x=1},.left={.y=1},.up={.z=1},.position={.z=-.1f}}; - for(int i=0;i<2;i++){ - vertices[i][0][0]=.15f+i*.05f;vertices[i][0][2]=.15f;vertices[i][1][1]=-.15f;vertices[i][2][2]=.3f+i*.1f; - buffers[i].data=(byte*)vertices[i];parts[i].vertex_buffer=(struct vertex_buffer){4,0,3,0,NULL,buffers+i}; - geometries[i].parts=(struct tag_block){1,parts+i,NULL}; - } - models[0].nodes=(struct tag_block){1,&node,NULL};models[0].geometries=(struct tag_block){2,geometries,NULL}; - equipment_count=3; - for(int i=0;i<3;i++){ - equipment[i].object.bounding_radius=.1f;equipment[i].object.render_bounding_radius=i==2?10:.1f; - equipment[i].object.animation_graph.index=NONE;equipment[i].object.model.index=0; - equipment[i].equipment.powerup_type=i==1?_equipment_powerup_health:_equipment_powerup_overshield; - } -} -static void tests(void){ - initialize();struct equipment_definition before[3];memcpy(before,equipment,sizeof(before)); - models_fix_powerup_render_bounds();CHECK(equipment[0].object.render_bounding_radius>.3f); - before[0].object.render_bounding_radius=equipment[0].object.render_bounding_radius;CHECK(!memcmp(before,equipment,sizeof(before))); - real grown=equipment[0].object.render_bounding_radius;models_fix_powerup_render_bounds();CHECK(grown==equipment[0].object.render_bounding_radius); - /* Atomic fallback: a valid first LOD must not mask a bad later LOD. */ - for(int c=0;c<14;c++){ - initialize();switch(c){case 0:models[0].nodes.count=2;break;case 1:parts[1].vertex_buffer.type=0;break; - case 2:parts[1].vertex_buffer.hardware_format=NULL;break;case 3:parts[1].vertex_buffer.count=0;break; - case 4:parts[1].flags=2;break;case 5:vertices[1][0][0]=NAN;break;case 6:models[0].geometries.address=NULL;break; - case 7:equipment[0].object.model.index=NONE;equipment[2].object.model.index=NONE;break; - case 8:equipment[0].object.animation_graph.index=5;equipment[2].object.animation_graph.index=5;break; - case 9:parts[1].vertex_buffer.offset=1;break;case 10:buffers[1].data=NULL;break;case 11:models[0].nodes.address=NULL;break;case 12:node.runtime_default_inverse_matrix.scale=NAN;break;case 13:models[0].geometries.count=257;break;} - memcpy(before,equipment,sizeof(before));models_fix_powerup_render_bounds();CHECK(!memcmp(before,equipment,sizeof(before))); - } - initialize();equipment[0].object.bounding_offset=(real_point3d){.x=.3f,.z=.1f}; - real radius;CHECK(model_rigid_render_radius(models,&equipment[0].object.bounding_offset,&radius));CHECK(radius>.39f); - parts[1].flags=1;CHECK(model_rigid_render_radius(models,&equipment[0].object.bounding_offset,&radius)); - /* compressed and uncompressed vertices have identical positions */ - float packed[3][8]={0};for(int i=0;i<3;i++)memcpy(packed[i],vertices[0][i],12); - parts[0].vertex_buffer.type=5;buffers[0].data=(byte*)packed; - CHECK(model_rigid_render_radius(models,&equipment[0].object.bounding_offset,&radius)); - object.definition_index=0;object.object.bounding_sphere_center=(real_point3d){.x=3,.y=4,.z=5}; - float scales[]={0,.1f,.5f,1,2,4}; - for(int i=0;i<6;i++){real_point3d center;object.object.scale=scales[i];object_get_render_bounding_sphere(0,¢er,&radius); - CHECK(!memcmp(¢er,&object.object.bounding_sphere_center,sizeof(center))); - CHECK(radius==equipment[0].object.render_bounding_radius*(scales[i]>1?scales[i]:1));} - puts("PASS: all LODs, root transform/offset, both vertex formats, conservative/idempotent growth, 14 atomic fallback paths, scaled culling; only render radius changes"); -} -static void frustum(struct render_frustum *f,float fov,float aspect){ - float tv=tanf(fov*.5f),th=tv*aspect,h=1/sqrtf(1+th*th),v=1/sqrtf(1+tv*tv); - f->world_bounds=(real_rectangle3d){-100*th,100*th,-100*tv,100*tv,-100,0}; - f->world_planes[0]=(real_plane3d){{.x=-h,.z=th*h},0};f->world_planes[1]=(real_plane3d){{.x=h,.z=th*h},0}; - f->world_planes[2]=(real_plane3d){{.y=-v,.z=tv*v},0};f->world_planes[3]=(real_plane3d){{.y=v,.z=tv*v},0}; - f->world_planes[4]=(real_plane3d){{.z=1},-.05f};f->world_planes[5]=(real_plane3d){{.z=-1},100}; -} -static void sweeps(struct model *model,struct equipment_definition *definition){ - real local_radius=definition->object.render_bounding_radius;int checks=0,legacy_failures=0,culled=0; - for(int size=0;size<4;size++)for(int fov=0;fov<3;fov++)for(int viewport=0;viewport<4;viewport++)for(int rotation=0;rotation<4;rotation++)for(int edge=0;edge<4;edge++)for(int step=-20;step<=20;step++){ - float scales[]={.25f,1,2,4},fovs[]={.6f,1.2f,1.8f},aspects[]={16.f/9,4.f/3,32.f/9,8.f/9}; - float scale=scales[size],depth=3,angle=rotation*.6f,tv=tanf(fovs[fov]*.5f),th=tv*aspects[viewport]; - real_matrix4x3 world={.scale=scale,.forward={.x=cosf(angle),.z=sinf(angle)},.left={.y=1},.up={.x=-sinf(angle),.z=cosf(angle)},.position={.z=-depth}}; - if(edge<2)world.position.x=(edge?1:-1)*(depth*th+step*.1f*local_radius*scale*sqrtf(1+th*th)); - else world.position.y=(edge==3?1:-1)*(depth*tv+step*.1f*local_radius*scale*sqrtf(1+tv*tv)); - struct render_frustum f;frustum(&f,fovs[fov],aspects[viewport]); - real_point3d center;matrix4x3_transform_point(&world,&definition->object.bounding_offset,¢er); - object.definition_index=0;equipment[0]=*definition;object.object.scale=scale;object.object.bounding_sphere_center=center; - real radius;real_point3d queried;object_get_render_bounding_sphere(0,&queried,&radius); - int visible=render_frustum_sphere_visible(&f,&queried,radius),has_vertex=0; - for(int g=0;ggeometries.count;g++){ - struct model_geometry *geo=TAG_BLOCK_GET_ELEMENT(&model->geometries,g,struct model_geometry); - for(int p=0;pparts.count;p++){ - struct vertex_buffer *buffer=&TAG_BLOCK_GET_ELEMENT(&geo->parts,p,struct model_geometry_part)->vertex_buffer; - struct test_buffer *b=buffer->hardware_format; - for(int i=0;icount;i++){ - real_point3d root,point;matrix4x3_transform_point(&((struct model_node*)model->nodes.address)->runtime_default_inverse_matrix,(real_point3d*)(b->data+i*rasterizer_geometry_get_vertex_size(buffer->type)),&root); - matrix4x3_transform_point(&world,&root,&point);CHECK(distance_squared3d(&point,¢er)<=radius*radius*1.00001f); - int inside=1;for(int plane=0;plane<6;plane++)inside&=plane3d_distance_to_point(&f.world_planes[plane],&point)<-.000001f; - has_vertex|=inside; - } - } - } - if(has_vertex){CHECK(visible);legacy_failures+=!render_frustum_sphere_visible(&f,¢er,.1f);} - culled+=!visible;checks++; - } - CHECK(legacy_failures>0&&culled>0);printf("PASS: %d edge sweeps; %d premature legacy culls; mesh containment, four viewport ratios, rotations, FOVs and scales; fully offscreen culling retained\n",checks,legacy_failures); -} -static void asset_test(const char *path){ - FILE *f=fopen(path,"rb");CHECK(f);unsigned int count;CHECK(fread(&count,4,1,f)==1&&count==2); - for(int entry=0;entry.1f); - before.object.render_bounding_radius=equipment[0].object.render_bounding_radius;CHECK(!memcmp(&before,equipment,sizeof(before))); - definition.object.render_bounding_radius=equipment[0].object.render_bounding_radius; - printf("Actual powerup type %d: radius %.6f -> %.6f\n",definition.equipment.powerup_type,.1f,radius);sweeps(&model,&definition); - for(int i=0;i1)asset_test(argv[1]);return 0;} -''' - -def fixture(path): - raw=path.read_bytes();data=raw if struct.unpack_from('