#include "../app/Gallery_Plot_Session.h" #include "../app/Gallery_Protocol.h" #include "../app/common/Pixel_Frame.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace renderive::web { namespace { std::uint32_t read_u32_le(const std::string& value, std::size_t offset) { return static_cast(value[offset]) | (static_cast(static_cast(value[offset + 1])) << 8U) | (static_cast(static_cast(value[offset + 2])) << 16U) | (static_cast(static_cast(value[offset + 3])) << 24U); } Gallery_Request gallery_request(Gallery_Request_Kind kind, std::string message) { return {kind, std::move(message)}; } std::string open_message(std::string_view mode) { return std::string( R"({"category":"event","type":"gallery_open","case":"datoviz_3d","frame_mode":")") + std::string(mode) + R"("})"; } nlohmann::json response_json(std::optional response) { if (!response) { ADD_FAILURE() << "expected a Web response"; return nlohmann::json::object(); } EXPECT_EQ(response->type, Web_Response_Type::Json); return nlohmann::json::parse(response->payload); } nlohmann::json open_scene(Gallery_Plot_Session& session, std::string_view mode) { return response_json(session.handle(gallery_request( Gallery_Request_Kind::Open, open_message(mode)))); } nlohmann::json invoke_action(Gallery_Plot_Session& session, std::string_view action_id) { const nlohmann::json request{{"category", "event"}, {"type", "gallery_action"}, {"action", action_id}}; return response_json(session.handle(gallery_request( Gallery_Request_Kind::Action, request.dump()))); } std::string request_pixels(Gallery_Plot_Session& session) { const auto response = session.handle(Frame_Request{}); if (!response) { ADD_FAILURE() << "expected a pixel response"; return {}; } EXPECT_EQ(response->type, Web_Response_Type::Pixels); return response->payload; } void expect_actual_datoviz_pixels(const std::string& frame, std::uint32_t expected_width, std::uint32_t expected_height) { ASSERT_GE(frame.size(), pixel_frame_header_size); ASSERT_EQ(frame.substr(0, 4), "RVP2"); ASSERT_EQ(read_u32_le(frame, 4), expected_width); ASSERT_EQ(read_u32_le(frame, 8), expected_height); ASSERT_EQ(read_u32_le(frame, 12), expected_width * 4U); ASSERT_EQ(frame.size(), pixel_frame_header_size + static_cast(expected_width) * expected_height * 4U); const auto* pixels = reinterpret_cast( frame.data() + pixel_frame_header_size); const std::array background{ pixels[0], pixels[1], pixels[2], pixels[3]}; std::size_t non_background{}; std::size_t red_dominant{}; std::size_t green_dominant{}; std::size_t blue_dominant{}; const std::size_t pixel_count = static_cast(expected_width) * expected_height; for (std::size_t index = 0; index < pixel_count; ++index) { const auto* pixel = pixels + index * 4U; non_background += pixel[0] != background[0] || pixel[1] != background[1] || pixel[2] != background[2] || pixel[3] != background[3]; red_dominant += pixel[0] > static_cast(pixel[1]) + 20U && pixel[0] > static_cast(pixel[2]) + 20U; green_dominant += pixel[1] > static_cast(pixel[0]) + 20U && pixel[1] > static_cast(pixel[2]) + 20U; blue_dominant += pixel[2] > static_cast(pixel[0]) + 20U && pixel[2] > static_cast(pixel[1]) + 20U; } EXPECT_GT(non_background, 0U); EXPECT_GT(red_dominant, 32U); EXPECT_GT(green_dominant, 32U); EXPECT_GT(blue_dominant, 32U); } std::optional blue_centroid_x(const std::string& frame) { if (frame.size() < pixel_frame_header_size) return std::nullopt; const std::uint32_t width = read_u32_le(frame, 4); const std::uint32_t height = read_u32_le(frame, 8); if (frame.size() != pixel_frame_header_size + static_cast(width) * height * 4U) return std::nullopt; const auto* pixels = reinterpret_cast( frame.data() + pixel_frame_header_size); double x_sum{}; std::size_t count{}; for (std::uint32_t y = 0; y < height; ++y) { for (std::uint32_t x = 0; x < width; ++x) { const auto* pixel = pixels + (static_cast(y) * width + x) * 4U; if (pixel[2] > static_cast(pixel[0]) + 20U && pixel[2] > static_cast(pixel[1]) + 20U) { x_sum += x; ++count; } } } return count == 0 ? std::nullopt : std::optional(x_sum / static_cast(count)); } void manual_render_cycle(Gallery_Plot_Session& session) { ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state"); ASSERT_EQ(invoke_action(session, "mode_refresh").at("type"), "case_state"); ASSERT_EQ(invoke_action(session, "mode_render").at("type"), "case_state"); } std::set action_ids(const nlohmann::json& state) { std::set result; for (const auto& action : state.at("actions").at("data")) result.insert(action.at("id").get()); return result; } TEST(RenderiveWebDatovizGallery, RgbaEncoderPreservesRowsAndRemovesPadding) { std::array source{}; const std::array expected{ 255, 1, 2, 255, 3, 254, 4, 255, 5, 6, 253, 255, 252, 7, 8, 255}; std::copy_n(expected.begin(), 8, source.begin()); std::copy_n(expected.begin() + 8, 8, source.begin() + 12); source[8] = source[9] = source[10] = source[11] = 99; const std::string frame = encode_rgba8_pixel_frame( reinterpret_cast(source.data()), 2, 2, 12, 42); ASSERT_EQ(frame.size(), pixel_frame_header_size + expected.size()); EXPECT_EQ(frame.substr(0, 4), "RVP2"); EXPECT_EQ(read_u32_le(frame, 4), 2U); EXPECT_EQ(read_u32_le(frame, 8), 2U); EXPECT_EQ(read_u32_le(frame, 12), 8U); EXPECT_TRUE(std::equal( expected.begin(), expected.end(), reinterpret_cast( frame.data() + pixel_frame_header_size))); EXPECT_TRUE(encode_rgba8_pixel_frame( reinterpret_cast(source.data()), 2, 2, 7, 42) .empty()); } TEST(RenderiveWebDatovizGallery, CatalogUsesOnlyThreeDimensionalActions) { const auto catalog = nlohmann::json::parse(Gallery_Protocol::catalog_json()); const std::set visual_cases{ "datoviz_3d", "datoviz_splat", "datoviz_pixel", "datoviz_marker", "datoviz_sphere", "datoviz_segment", "datoviz_vector", "datoviz_primitive", "datoviz_mesh", "datoviz_path", "datoviz_image", "datoviz_labels", "datoviz_glyph", "datoviz_text", "datoviz_volume"}; for (const auto& id : visual_cases) { EXPECT_NE(std::find_if( catalog.at("cases").begin(), catalog.at("cases").end(), [&id](const nlohmann::json& value) { return value.at("id") == id; }), catalog.at("cases").end()) << id; } const auto item = std::find_if( catalog.at("cases").begin(), catalog.at("cases").end(), [](const nlohmann::json& value) { return value.at("id") == "datoviz_3d"; }); ASSERT_NE(item, catalog.at("cases").end()); EXPECT_EQ(item->at("category"), "Datoviz Visuals"); EXPECT_EQ(item->at("control_count"), 0); EXPECT_EQ(item->at("control_count_by_mode").at("manual"), 0); EXPECT_EQ(item->at("control_count_by_mode").at("low_latency"), 0); EXPECT_EQ(item->at("control_count_by_mode").at("playback"), 0); const std::string empty_controls = R"({"resources":[],"observers":[],"render_plan":null,"performance_capture":null})"; const auto state_for = [&](Gallery_Frame_Mode mode) { return nlohmann::json::parse(Gallery_Protocol::case_json_from_controls( "datoviz_3d", empty_controls, "{}", {}, mode, false)); }; const std::set common{ "add_mesh", "change_text", "move_mesh", "remove_mesh", "reset", "reset_camera", "mesh_churn", "toggle_projection", "toggle_texture"}; auto expected = common; expected.insert({"mode_prepare", "mode_refresh", "mode_render"}); EXPECT_EQ(action_ids(state_for(Gallery_Frame_Mode::Manual)), expected); expected = common; expected.insert({"mode_prepare", "mode_render"}); EXPECT_EQ(action_ids(state_for(Gallery_Frame_Mode::Low_Latency)), expected); expected = common; expected.insert({"mode_enqueue", "mode_dequeue"}); EXPECT_EQ(action_ids(state_for(Gallery_Frame_Mode::Playback)), expected); EXPECT_FALSE(action_ids(state_for(Gallery_Frame_Mode::Manual)).contains("mode_discard")); EXPECT_FALSE(action_ids(state_for(Gallery_Frame_Mode::Low_Latency)).contains("mode_discard")); } TEST(RenderiveWebDatovizGallery, PublishesRealRgbaAndRendersAfterResize) { Gallery_Plot_Session session; ASSERT_EQ(open_scene(session, "low_latency").at("type"), "case_state"); const std::string initial = request_pixels(session); expect_actual_datoviz_pixels(initial, 560, 320); EXPECT_FALSE(session.handle(Viewport_Resize{{900, 500}}).has_value()); ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state"); ASSERT_EQ(invoke_action(session, "mode_render").at("type"), "case_state"); const std::string resized = request_pixels(session); expect_actual_datoviz_pixels(resized, 900, 500); const auto observed = response_json(session.handle(gallery_request( Gallery_Request_Kind::Observe, R"({"category":"event","type":"gallery_observe","client_metrics":{}})"))); EXPECT_TRUE(observed.at("telemetry").contains("scheduler")); const auto& performance = observed.at("telemetry").at("performance"); EXPECT_GT(performance.at("measured_fps").get(), 0.0); EXPECT_GT(performance.at("pixel_response_fps").get(), 0.0); EXPECT_GT(performance.at("last_render_ms").get(), 0.0); EXPECT_GT(performance.at("last_pixel_encode_ms").get(), 0.0); } TEST(RenderiveWebDatovizGallery, AutomaticLowLatencyFramesCarryAnimatedState) { Gallery_Plot_Session session(true); ASSERT_EQ(open_scene(session, "low_latency").at("type"), "case_state"); const std::string first = request_pixels(session); expect_actual_datoviz_pixels(first, 560, 320); std::string later; const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); do { std::this_thread::sleep_for(std::chrono::milliseconds(40)); later = request_pixels(session); } while (later == first && std::chrono::steady_clock::now() < deadline); ASSERT_NE(later, first); expect_actual_datoviz_pixels(later, 560, 320); } TEST(RenderiveWebDatovizGallery, ManualRenderUsesThePreparedLogicalState) { Gallery_Plot_Session session; ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state"); ASSERT_EQ(invoke_action(session, "move_mesh").at("type"), "case_state"); ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state"); ASSERT_EQ(invoke_action(session, "move_mesh").at("type"), "case_state"); ASSERT_EQ(invoke_action(session, "mode_refresh").at("type"), "case_state"); ASSERT_EQ(invoke_action(session, "mode_render").at("type"), "case_state"); const std::string prepared = request_pixels(session); expect_actual_datoviz_pixels(prepared, 560, 320); manual_render_cycle(session); const std::string current = request_pixels(session); expect_actual_datoviz_pixels(current, 560, 320); EXPECT_NE(current, prepared); } TEST(RenderiveWebDatovizGallery, PlaybackDequeuesHistoricalStatesInOrder) { Gallery_Plot_Session session; ASSERT_EQ(open_scene(session, "playback").at("type"), "case_state"); for (int index = 0; index < 20; ++index) ASSERT_EQ(invoke_action(session, "mode_enqueue").at("type"), "case_state"); ASSERT_EQ(invoke_action(session, "mode_dequeue").at("type"), "case_state"); const std::string first = request_pixels(session); const auto first_x = blue_centroid_x(first); ASSERT_TRUE(first_x.has_value()); ASSERT_EQ(invoke_action(session, "mode_dequeue").at("type"), "case_state"); const std::string second = request_pixels(session); const auto second_x = blue_centroid_x(second); ASSERT_TRUE(second_x.has_value()); EXPECT_GT(*second_x, *first_x); std::string last = second; for (int index = 2; index < 20; ++index) { ASSERT_EQ(invoke_action(session, "mode_dequeue").at("type"), "case_state"); last = request_pixels(session); } const auto last_x = blue_centroid_x(last); ASSERT_TRUE(last_x.has_value()); EXPECT_GT(*last_x, *second_x); EXPECT_NE(last, second); } TEST(RenderiveWebDatovizGallery, AddedAndChurnedVisualsAreActuallyRemoved) { Gallery_Plot_Session session; ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state"); const std::string baseline = request_pixels(session); ASSERT_EQ(invoke_action(session, "add_mesh").at("type"), "case_state"); manual_render_cycle(session); const std::string added = request_pixels(session); EXPECT_NE(added, baseline); const auto centroid_y = [&](const auto& matches) -> std::optional { const auto* pixels = reinterpret_cast( added.data() + pixel_frame_header_size); double y_sum{}; std::size_t count{}; for (std::uint32_t y = 0; y < 320; ++y) { for (std::uint32_t x = 0; x < 560; ++x) { const auto* pixel = pixels + (static_cast(y) * 560U + x) * 4U; if (matches(pixel[0], pixel[1], pixel[2])) { y_sum += y; ++count; } } } return count == 0 ? std::nullopt : std::optional(y_sum / static_cast(count)); }; const auto green_y = centroid_y([](unsigned red, unsigned green, unsigned blue) { return green > red + 20U && green > blue + 20U; }); const auto yellow_y = centroid_y([](unsigned red, unsigned green, unsigned blue) { return red > 100U && green > 100U && blue < 100U; }); ASSERT_TRUE(green_y.has_value()); ASSERT_TRUE(yellow_y.has_value()); EXPECT_LT(*yellow_y, *green_y) << "Frame_3D_View must expose top-left-origin RGBA rows"; ASSERT_EQ(invoke_action(session, "remove_mesh").at("type"), "case_state"); manual_render_cycle(session); const std::string removed = request_pixels(session); ASSERT_GE(removed.size(), pixel_frame_header_size); ASSERT_GE(baseline.size(), pixel_frame_header_size); EXPECT_EQ(removed.substr(pixel_frame_header_size), baseline.substr(pixel_frame_header_size)); const auto churned = invoke_action(session, "mesh_churn"); ASSERT_EQ(churned.at("type"), "case_state"); EXPECT_EQ(churned.at("telemetry").at("kernel_observer").at("last_event"), "mesh_churned"); const std::string after_churn = request_pixels(session); expect_actual_datoviz_pixels(after_churn, 560, 320); EXPECT_EQ(after_churn.substr(pixel_frame_header_size), baseline.substr(pixel_frame_header_size)); } TEST(RenderiveWebDatovizGallery, TextureAndTextActionsReachTheThreeDimensionalBackend) { Gallery_Plot_Session session; ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state"); const std::string baseline = request_pixels(session); expect_actual_datoviz_pixels(baseline, 560, 320); ASSERT_EQ(invoke_action(session, "toggle_texture").at("type"), "case_state"); manual_render_cycle(session); const std::string without_texture = request_pixels(session); ASSERT_GE(without_texture.size(), pixel_frame_header_size); ASSERT_GE(baseline.size(), pixel_frame_header_size); EXPECT_NE(without_texture.substr(pixel_frame_header_size), baseline.substr(pixel_frame_header_size)); ASSERT_EQ(invoke_action(session, "toggle_texture").at("type"), "case_state"); manual_render_cycle(session); const std::string restored_texture = request_pixels(session); ASSERT_GE(restored_texture.size(), pixel_frame_header_size); EXPECT_EQ(restored_texture.substr(pixel_frame_header_size), baseline.substr(pixel_frame_header_size)); ASSERT_EQ(invoke_action(session, "change_text").at("type"), "case_state"); manual_render_cycle(session); const std::string changed_text = request_pixels(session); ASSERT_GE(changed_text.size(), pixel_frame_header_size); EXPECT_NE(changed_text.substr(pixel_frame_header_size), restored_texture.substr(pixel_frame_header_size)); } } // namespace } // namespace renderive::web