#include "prism/renderer.hpp" #include "prism/memory_lab.hpp" #include "prism/raster_lab.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace { std::size_t checks{}; void require(bool condition, const char* message) { ++checks; if (!condition) throw std::runtime_error{message}; } prism::Scene minimal() { return {1, "Test scene", 1, 1, {{0, 0, 3}, {0, 0, 0}, 60}, {0, 1, 0}, {0, 0, 1}, -1, {1, 1, 1}, {}}; } void domain_contract() { auto scene = minimal(); require(prism::validate_scene(scene).has_value(), "minimal scene is valid"); scene.version = 2; require(!prism::validate_scene(scene), "unsupported typed scene contract is rejected"); scene = minimal(); scene.width = 0; require(!prism::render(scene), "zero width is rejected at render boundary"); scene = minimal(); scene.width = prism::max_dimension + 1; require(!prism::validate_scene(scene), "dimension limit is enforced"); scene = minimal(); scene.width = 2048; scene.height = 1024; require(prism::validate_scene(scene).has_value(), "exact pixel budget is valid"); scene.height = 1025; require(!prism::validate_scene(scene), "pixel product beyond budget is rejected"); scene = minimal(); scene.camera.look_at = scene.camera.position; require(!prism::validate_scene(scene), "degenerate camera is rejected"); scene = minimal(); scene.camera.look_at = {0, 1, 3}; require(!prism::validate_scene(scene), "world-up parallel camera is rejected"); scene = minimal(); scene.light_direction = {0, 0, 0}; require(!prism::validate_scene(scene), "zero light cannot be normalized"); scene = minimal(); scene.background.r = std::numeric_limits::quiet_NaN(); require(!prism::validate_scene(scene), "NaN is rejected for caller-built scenes"); scene = minimal(); scene.ground_y = std::numeric_limits::infinity(); require(!prism::validate_scene(scene), "infinity is rejected for caller-built scenes"); scene = minimal(); scene.spheres.push_back({{0, 0, 0}, 0, {1, 0, 0}}); require(!prism::validate_scene(scene), "zero radius is rejected"); scene = minimal(); scene.spheres.resize(prism::max_spheres + 1, {{0, 0, 0}, 1, {1, 0, 0}}); require(!prism::validate_scene(scene), "sphere count is bounded"); scene.spheres.resize(prism::max_spheres); scene.width = 2048; scene.height = 1024; require(!prism::validate_scene(scene), "combined scene/render work is bounded"); scene = minimal(); scene.title = "bad\nname"; require(!prism::validate_scene(scene), "diagnostic title controls are rejected"); scene = minimal(); scene.camera.vertical_fov_degrees = 121; require(!prism::validate_scene(scene), "field of view is bounded"); scene = minimal(); scene.spheres.push_back({{1001, 0, 0}, 1, {1, 0, 0}}); require(!prism::validate_scene(scene), "sphere coordinates are bounded before math"); auto owner = prism::demo_scene(); const auto copy = owner; owner.title.assign("changed"); owner.spheres.clear(); require(copy.title != owner.title && copy.spheres.size() == 3, "typed scenes own independent copied storage"); } void rendering_contract() { auto scene = minimal(); const auto background = prism::render(scene); require(background && background->rgb == std::vector{0, 255, 0}, "central empty ray returns exact green background"); scene.spheres.push_back({{0, 0, 0}, 1, {1, 0, 0}}); const auto sphere = prism::render(scene); require(sphere && sphere->rgb == std::vector{255, 0, 0}, "sphere ray intersection and front light give exact red"); scene.spheres.push_back({{0, 0, -3}, 1, {0, 0, 1}}); const auto near_first = prism::render(scene); std::swap(scene.spheres[0], scene.spheres[1]); const auto far_first = prism::render(scene); require(near_first && far_first && near_first->rgb == sphere->rgb && far_first->rgb == sphere->rgb, "nearest positive sphere hit wins in both submission orders"); scene.spheres = {{{1, 0, 0}, 1, {1, 0, 0}}}; scene.light_direction = {-1, 0, 0}; const auto tangent = prism::render(scene); require(tangent && tangent->rgb == std::vector{255, 0, 0}, "axis-aligned tangent ray has one accepted lit hit"); scene.spheres = {{{0, 0, 0}, 1, {1, 0, 0}}}; scene.light_direction = {1, 0, 1}; const auto lit = prism::render(scene); scene.spheres.push_back({{1, 0, 2}, 0.25, {1, 1, 1}}); const auto shadowed = prism::render(scene); require(lit && shadowed && lit->rgb[0] > 108 && shadowed->rgb == std::vector{108, 0, 0}, "sphere outside camera ray blocks directional light and leaves ambient red"); scene.spheres.pop_back(); scene.light_direction = {0, 0, 1}; scene.camera.position = {0, 0, 0}; scene.camera.look_at = {0, 0, -1}; const auto inside = prism::render(scene); require(inside && inside->rgb == std::vector{108, 0, 0}, "inside sphere exit root shades ambient red rather than green background"); scene = minimal(); scene.camera.position = {0, 0, 1000}; scene.camera.look_at = {0, 0, -1000}; scene.spheres = {{{0, 0, -999}, 0.001, {1, 0, 0}}}; const auto small_distant = prism::render(scene); require(small_distant && small_distant->rgb == std::vector{255, 0, 0}, "supported minimum-radius sphere has a finite lit central hit at large distance"); scene = minimal(); scene.width = 32; scene.height = 24; const auto first = prism::render(scene), second = prism::render(scene); require(first && second && first->rgb == second->rgb, "render is deterministic for repeated input"); require(first->pixels().size() == 32 * 24 * 3, "image owns exactly width*height*3 RGB bytes"); const auto demo = prism::demo_scene(); require(prism::validate_scene(demo).has_value(), "typed studio scene is valid"); const auto rendered = prism::render(demo); require(rendered && rendered->rgb.size() == 960 * 600 * 3, "studio scene renders its complete image"); } void ppm_contract() { // No shared fixed temporary filename: isolate the test output in a unique // directory and clean it through an RAII guard even if an assertion throws. const auto base = std::filesystem::temp_directory_path(); std::filesystem::path folder; for (unsigned i = 0; i < 1000; ++i) { auto candidate = base / ("prism-native-test-" + std::to_string(i)); if (std::filesystem::create_directory(candidate)) { folder = std::move(candidate); break; } } require(!folder.empty(), "temporary test directory created"); struct Cleanup { std::filesystem::path path; ~Cleanup() { std::error_code e; std::filesystem::remove_all(path, e); } } cleanup{folder}; const auto image = prism::render(minimal()); const auto path = folder / "pixel.ppm"; require(prism::write_ppm(path, *image).has_value(), "PPM writes successfully"); std::ifstream file{path, std::ios::binary}; const std::string contents{std::istreambuf_iterator{file}, {}}; const std::string expected = std::string{"P6\n1 1\n255\n"} + std::string{"\0\xff\0", 3}; require(contents == expected, "PPM header and payload are exact"); auto invalid_image = *image; invalid_image.rgb.clear(); require(!prism::write_ppm(folder / "bad.ppm", invalid_image), "image shape mismatch is rejected before opening output"); require(!std::filesystem::exists(folder / "bad.ppm"), "invalid image creates no output"); require(!prism::write_ppm(folder / "missing" / "out.ppm", *image), "unwritable destination returns I/O error"); struct GroupedNumbers final : std::numpunct { char do_thousands_sep() const override { return ','; } std::string do_grouping() const override { return "\3"; } }; struct RestoreLocale { std::locale previous; ~RestoreLocale() { std::locale::global(previous); } } restore{std::locale{}}; // Independent of any installed locale: exercise a valid embedding app's // grouping convention without allowing it into the PPM numeric grammar. std::locale::global(std::locale{std::locale::classic(), new GroupedNumbers}); const prism::Image wide{1000, 1, std::vector(3000)}; const auto localized_path = folder / "localized.ppm"; require(prism::write_ppm(localized_path, wide).has_value(), "PPM writes under a grouped global locale"); std::ifstream localized_file{localized_path, std::ios::binary}; const std::string localized_contents{std::istreambuf_iterator{localized_file}, {}}; require(localized_contents.starts_with("P6\n1000 1\n255\n") && localized_contents.size() == 3014, "PPM header remains ungrouped ASCII under a grouped global locale"); } void allocator_contract() { const auto report = prism::run_memory_lab(); require(report.bounded_failure, "bounded arena exhausts and preserves existing vector values"); require(report.live_after_scope == 0, "all borrowed containers die before resource release"); require(report.allocations >= 1 && report.peak_bytes >= 16 * sizeof(int) && report.peak_bytes <= 512, "allocation accounting covers the vector without assuming a string's small-buffer capacity"); require(report.promoted_title == "A title promoted beyond the arena lifetime", "promoted owner survives arena reset and destruction"); prism::CountingResource failing{*std::pmr::null_memory_resource()}; bool failed = false; try { std::pmr::vector values{&failing}; values.reserve(1); } catch (const std::bad_alloc&) { failed = true; } require(failed && failing.allocations() == 0 && failing.live_bytes() == 0, "first-allocation failure leaves counters unchanged"); } prism::Triangle flat_triangle(double depth, prism::Color color) { return {{{{0, 0, depth, color}, {2, 0, depth, color}, {0, 2, depth, color}}}}; } std::array pixel(const prism::Image& image, std::size_t x, std::size_t y) { const auto offset = (y * image.width + x) * 3; return {image.rgb.at(offset), image.rgb.at(offset + 1), image.rgb.at(offset + 2)}; } void raster_contract() { prism::RasterScene scene{2, 2, {0, 0, 0}, {flat_triangle(0.5, {1, 0, 0})}}; auto image = prism::rasterize(scene); require(image && image->rgb.size() == 12, "raster image has exact RGB storage"); require(pixel(*image, 0, 0) == std::array{255, 0, 0}, "edge functions cover an interior pixel"); require(pixel(*image, 1, 1) == std::array{0, 0, 0}, "edge functions reject an exterior pixel"); std::swap(scene.triangles[0].vertices[1], scene.triangles[0].vertices[2]); auto reversed = prism::rasterize(scene); require(reversed && reversed->rgb == image->rgb, "both triangle windings produce identical coverage"); scene.triangles[0] = {{{{0, 0, 0.5, {1, 0, 0}}, {2, 0, 0.5, {0, 1, 0}}, {0, 2, 0.5, {0, 0, 1}}}}}; image = prism::rasterize(scene); require(image && pixel(*image, 0, 0) == std::array{180, 128, 128}, "barycentric colors interpolate in linear RGB before gamma encoding"); scene.triangles = {flat_triangle(0.8, {1, 0, 0}), flat_triangle(0.2, {0, 0, 1})}; const auto far_first = prism::rasterize(scene); std::swap(scene.triangles[0], scene.triangles[1]); const auto near_first = prism::rasterize(scene); require(far_first && near_first && far_first->rgb == near_first->rgb, "depth buffer chooses nearest independent of submission order"); require(pixel(*near_first, 0, 0) == std::array{0, 0, 255}, "near triangle owns the overlap pixel"); scene.triangles = {flat_triangle(0.2, {1, 0, 0}), flat_triangle(0.2, {0, 0, 1})}; image = prism::rasterize(scene); require(image && pixel(*image, 0, 0) == std::array{255, 0, 0}, "exact depth ties preserve first submitted sample"); const prism::Triangle lower{{{{2, 0, 0.5, {0, 0, 1}}, {2, 2, 0.5, {0, 0, 1}}, {0, 2, 0.5, {0, 0, 1}}}}}; scene.triangles = {flat_triangle(0.5, {1, 0, 0}), lower}; const auto shared_first = prism::rasterize(scene); std::swap(scene.triangles[0], scene.triangles[1]); const auto shared_second = prism::rasterize(scene); require(shared_first && shared_second && pixel(*shared_first, 1, 0) == std::array{255, 0, 0} && pixel(*shared_second, 1, 0) == std::array{0, 0, 255}, "inclusive shared edge is covered by both triangles and first exact tie wins"); scene.triangles = {flat_triangle(1.0, {1, 0, 0})}; const auto far_boundary = prism::rasterize(scene); require(far_boundary && pixel(*far_boundary, 0, 0) == std::array{255, 0, 0}, "far-depth endpoint remains drawable against the infinite clear value"); scene.width = 4; scene.height = 4; const prism::Triangle varying{{{{0, 0, 0, {1, 0, 0}}, {4, 0, 1, {1, 0, 0}}, {0, 4, 1, {1, 0, 0}}}}}; const prism::Triangle constant{{{{0, 0, 0.5, {0, 1, 0}}, {4, 0, 0.5, {0, 1, 0}}, {0, 4, 0.5, {0, 1, 0}}}}}; scene.triangles = {varying, constant}; image = prism::rasterize(scene); require(image && pixel(*image, 0, 0) == std::array{255, 0, 0} && pixel(*image, 2, 0) == std::array{0, 255, 0}, "interpolated per-pixel depth changes which triangle wins"); scene = {2, 2, {0, 0, 0}, {{{{{0, 0, 0.5, {1, 0, 0}}, {1, 1, 0.5, {1, 0, 0}}, {2, 2, 0.5, {1, 0, 0}}}}}}}; image = prism::rasterize(scene); require(image && image->rgb == std::vector(12, 0), "degenerate triangle covers nothing and never divides by zero"); scene.triangles[0] = {{{{-2, -2, 0.5, {1, 0, 0}}, {4, -2, 0.5, {1, 0, 0}}, {-2, 4, 0.5, {1, 0, 0}}}}}; image = prism::rasterize(scene); require(image && pixel(*image, 0, 0)[0] == 255 && image->rgb.size() == 12, "partly off-screen bounding box is clamped before indexing"); scene.triangles[0] = {{{{-20, -20, 0.5, {1, 0, 0}}, {-18, -20, 0.5, {1, 0, 0}}, {-20, -18, 0.5, {1, 0, 0}}}}}; image = prism::rasterize(scene); require(image && image->rgb == std::vector(12, 0), "fully off-screen triangle performs no pixel writes"); scene.triangles = {flat_triangle(0.5, {1, 0, 0})}; scene.triangles[0].vertices[0].x = std::numeric_limits::infinity(); require(!prism::rasterize(scene), "non-finite screen coordinate is rejected before conversion"); scene.triangles[0] = flat_triangle(0.5, {1, 0, 0}); scene.triangles[0].vertices[0].x = prism::max_screen_coordinate + 1; require(!prism::validate_raster_scene(scene), "screen-coordinate bound is enforced"); scene.triangles[0] = flat_triangle(-0.01, {1, 0, 0}); require(!prism::validate_raster_scene(scene), "out-of-domain depth is rejected"); scene.triangles[0] = flat_triangle(std::numeric_limits::quiet_NaN(), {1, 0, 0}); require(!prism::validate_raster_scene(scene), "NaN depth is rejected"); scene.triangles.assign(prism::max_triangles + 1, flat_triangle(0.5, {1, 0, 0})); require(!prism::validate_raster_scene(scene), "triangle count is bounded"); scene.triangles.resize(8); scene.width = 2048; scene.height = 1024; require(prism::validate_raster_scene(scene).has_value(), "exact raster-work budget is valid"); scene.triangles.push_back(flat_triangle(0.5, {1, 0, 0})); require(!prism::validate_raster_scene(scene), "combined raster-work budget is enforced"); const auto demo = prism::rasterize(prism::demo_raster_scene()); require(demo && demo->rgb.size() == 960 * 600 * 3, "typed raster demo produces its full image"); } } // namespace int main() { try { domain_contract(); rendering_contract(); ppm_contract(); allocator_contract(); raster_contract(); std::cout << checks << " native contract checks passed.\n"; return 0; } catch (const std::exception& error) { std::cerr << "Native test failed: " << error.what() << '\n'; return 1; } }