#include "prism/raster_lab.hpp" #include #include #include #include namespace { using prism::ScreenVertex; 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; } double edge(const ScreenVertex& a, const ScreenVertex& b, double x, double y) noexcept { return (b.x - a.x) * (y - a.y) - (b.y - a.y) * (x - a.x); } std::uint8_t encode(double linear) { return static_cast(std::lround(std::sqrt(std::clamp(linear, 0.0, 1.0)) * 255.0)); } prism::Error invalid(const char* message) { return {prism::ErrorCode::invalid_scene, message}; } } // namespace namespace prism { RasterScene demo_raster_scene() { // Three overlapping projected triangles. The front triangle has varying // vertex colors and depth; submission order is deliberately not depth order. return {960, 600, {0.035, 0.055, 0.10}, { {{{{160, 100, 0.7, {0.10, 0.38, 0.85}}, {810, 170, 0.7, {0.10, 0.38, 0.85}}, {670, 530, 0.7, {0.10, 0.38, 0.85}}}}}, {{{{220, 470, 0.25, {0.98, 0.21, 0.10}}, {470, 85, 0.40, {0.98, 0.70, 0.18}}, {780, 440, 0.20, {0.12, 0.78, 0.59}}}}}, {{{{95, 390, 0.9, {0.16, 0.48, 0.46}}, {400, 200, 0.9, {0.16, 0.48, 0.46}}, {360, 545, 0.9, {0.16, 0.48, 0.46}}}}} }}; } std::expected validate_raster_scene(const RasterScene& scene) { if (scene.width == 0 || scene.height == 0 || scene.width > max_dimension || scene.height > max_dimension) return std::unexpected(invalid("Raster dimensions must each be in 1–2048.")); const auto width = static_cast(scene.width); const auto height = static_cast(scene.height); if (height > max_pixels / width) return std::unexpected(invalid("Raster exceeds the pixel budget.")); if (scene.triangles.size() > max_triangles) return std::unexpected(invalid("Raster exceeds the 64-triangle limit.")); const auto pixels = width * height; if (!scene.triangles.empty() && pixels > max_raster_work / scene.triangles.size()) return std::unexpected(invalid("Raster exceeds the pixel-times-triangle work budget.")); if (!valid_color(scene.background)) return std::unexpected(invalid("Raster background needs finite RGB in 0–1.")); for (const auto& triangle : scene.triangles) { for (const auto& vertex : triangle.vertices) { if (!std::isfinite(vertex.x) || !std::isfinite(vertex.y) || std::abs(vertex.x) > max_screen_coordinate || std::abs(vertex.y) > max_screen_coordinate || !std::isfinite(vertex.depth) || vertex.depth < 0 || vertex.depth > 1 || !valid_color(vertex.color)) return std::unexpected(invalid("Vertex needs bounded finite screen coordinates, depth in 0–1, and RGB in 0–1.")); } } return {}; } std::expected rasterize(const RasterScene& scene) { if (auto checked = validate_raster_scene(scene); !checked) return std::unexpected(checked.error()); const auto width = static_cast(scene.width); const auto count = width * static_cast(scene.height); // Validated product. if (count > std::numeric_limits::max() / 3) return std::unexpected(invalid("Raster RGB byte count would overflow.")); try { Image image{scene.width, scene.height, std::vector(count * 3)}; std::vector depths(count, std::numeric_limits::infinity()); for (std::size_t pixel = 0; pixel < count; ++pixel) { image.rgb[pixel * 3] = encode(scene.background.r); image.rgb[pixel * 3 + 1] = encode(scene.background.g); image.rgb[pixel * 3 + 2] = encode(scene.background.b); } for (const auto& triangle : scene.triangles) { const auto& a = triangle.vertices[0]; const auto& b = triangle.vertices[1]; const auto& c = triangle.vertices[2]; const double area = edge(a, b, c.x, c.y); if (std::abs(area) <= 1e-12) continue; // A degenerate primitive covers nothing. // Finite ±8192 input makes floor/ceil -> int conversions representable. // Clamp the box BEFORE looping or indexing; off-screen geometry is safe. const int min_x = std::max(0, static_cast(std::floor(std::min({a.x, b.x, c.x})))); const int min_y = std::max(0, static_cast(std::floor(std::min({a.y, b.y, c.y})))); const int max_x = std::min(static_cast(scene.width) - 1, static_cast(std::ceil(std::max({a.x, b.x, c.x}))) - 1); const int max_y = std::min(static_cast(scene.height) - 1, static_cast(std::ceil(std::max({a.y, b.y, c.y}))) - 1); for (int y = min_y; y <= max_y; ++y) { for (int x = min_x; x <= max_x; ++x) { const double px = static_cast(x) + 0.5; const double py = static_cast(y) + 0.5; const double wa = edge(b, c, px, py) / area; const double wb = edge(c, a, px, py) / area; const double wc = edge(a, b, px, py) / area; if (wa < 0 || wb < 0 || wc < 0) continue; // Works for either winding. const double depth = wa * a.depth + wb * b.depth + wc * c.depth; const auto index = static_cast(y) * width + static_cast(x); if (depth >= depths[index]) continue; // Near wins; exact ties keep the first. depths[index] = depth; image.rgb[index * 3] = encode(wa * a.color.r + wb * b.color.r + wc * c.color.r); image.rgb[index * 3 + 1] = encode(wa * a.color.g + wb * b.color.g + wc * c.color.g); image.rgb[index * 3 + 2] = encode(wa * a.color.b + wb * b.color.b + wc * c.color.b); } } } return image; // Both owners clean up automatically on return or exception. } catch (const std::bad_alloc&) { return std::unexpected(Error{ErrorCode::allocation, "Raster image or depth allocation failed."}); } } } // namespace prism