#include "prism/scene.hpp" #include #include namespace { prism::Error invalid(std::string message) { return {prism::ErrorCode::invalid_scene, std::move(message)}; } bool finite(double value, double bound = 1000.0) { return std::isfinite(value) && std::abs(value) <= bound; } bool valid_vector(prism::Vec3 v) { return finite(v.x) && finite(v.y) && finite(v.z); } double length_squared(prism::Vec3 v) { return v.x * v.x + v.y * v.y + v.z * v.z; } bool valid_color(prism::Color c) { return std::isfinite(c.r) && std::isfinite(c.g) && std::isfinite(c.b) && c.r >= 0 && c.r <= 1 && c.g >= 0 && c.g <= 1 && c.b >= 0 && c.b <= 1; } } // namespace namespace prism { Scene demo_scene() { // Typed authoring keeps this graphics starter independent of file formats. // The returned value owns its title and sphere storage. return {1, "Parallax · Afternoon study", 960, 600, {{5.2, 3.0, 7.5}, {0, 0.7, 0}, 38}, {0.76, 0.85, 0.96}, {-3, 6, 4}, 0, {0.82, 0.79, 0.73}, {{{-1.25, 1.05, 0}, 1.05, {0.93, 0.25, 0.10}}, {{1.05, 0.76, 0.25}, 0.76, {0.10, 0.37, 0.74}}, {{0.25, 0.42, 1.75}, 0.42, {0.29, 0.73, 0.59}}}}; } std::expected validate_scene(const Scene& scene) { if (scene.version != 1) return std::unexpected(invalid("Unsupported scene version; expected 1.")); if (scene.title.empty() || scene.title.size() > 80) return std::unexpected(invalid("Title must contain 1–80 bytes.")); for (const char byte : scene.title) { const auto c = static_cast(byte); if (c < 0x20 || c == 0x7f) return std::unexpected(invalid("Title contains a control character.")); } if (scene.width == 0 || scene.height == 0 || scene.width > max_dimension || scene.height > max_dimension) return std::unexpected(invalid("Image dimensions must each be in 1–2048.")); // Divide before multiplying: these checks remain correct on 32-bit size_t. const auto width = static_cast(scene.width); const auto height = static_cast(scene.height); if (height > max_pixels / width) return std::unexpected(invalid("Image exceeds the 2097152-pixel budget.")); const auto pixels = width * height; if (scene.spheres.size() > max_spheres) return std::unexpected(invalid("Scene exceeds the 32-sphere limit.")); // Shadow rays inspect every sphere too. Bound the primary intersection // budget here; the renderer performs at most twice this sphere work. if (!scene.spheres.empty() && pixels > max_intersection_work / scene.spheres.size()) return std::unexpected(invalid("Image and sphere count exceed the intersection budget.")); if (!valid_vector(scene.camera.position) || !valid_vector(scene.camera.look_at)) return std::unexpected(invalid("Camera coordinates must be finite and within ±1000.")); const Vec3 forward{scene.camera.look_at.x - scene.camera.position.x, scene.camera.look_at.y - scene.camera.position.y, scene.camera.look_at.z - scene.camera.position.z}; if (length_squared(forward) < 1e-12 || forward.x * forward.x + forward.z * forward.z < 1e-12) return std::unexpected(invalid("Camera needs a distinct target and cannot point parallel to world up.")); if (!finite(scene.camera.vertical_fov_degrees, 120) || scene.camera.vertical_fov_degrees < 10) return std::unexpected(invalid("Vertical field of view must be in 10–120 degrees.")); if (!valid_vector(scene.light_direction) || length_squared(scene.light_direction) < 1e-12) return std::unexpected(invalid("Light direction must be finite, bounded, and nonzero.")); if (!valid_color(scene.background) || !valid_color(scene.ground_color) || !finite(scene.ground_y)) return std::unexpected(invalid("Ground and background values are outside their domains.")); for (const auto& sphere : scene.spheres) { if (!valid_vector(sphere.center) || !finite(sphere.radius, 100) || sphere.radius < 0.001 || !valid_color(sphere.color)) return std::unexpected(invalid("Sphere needs bounded finite coordinates, radius in 0.001–100, and RGB in 0–1.")); } return {}; } } // namespace prism