Files
Renderive/web_server/tests/Web_Bridge_Tests.cpp
T
2026-08-12 06:16:00 +08:00

1312 lines
69 KiB
C++

#include "../app/Gallery_Plot_Session.h"
#include "../app/Gallery_Protocol.h"
#include "../app/Gallery_Renderables.h"
#include "../app/Pixel_Frame.h"
#include "../app/Web_Event_Adapter.h"
#include "../app/Web_Plot_Session.h"
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstring>
#include <memory>
#include <set>
#include <string>
#include <string_view>
#include <thread>
#include <variant>
#include <vector>
namespace renderive::web {
namespace {
struct Spatial_Test_Event {
int layer{};
};
struct Spatial_Test_Decoder {
using event_type = Spatial_Test_Event;
static std::optional<event_type> decode(const Json::Value& root, std::string_view) {
if (root["type"].asString() != "spatial_test" || !root["layer"].isInt())
return std::nullopt;
return Spatial_Test_Event{root["layer"].asInt()};
}
};
using Extended_Web_Event = Basic_Web_Event<Event, Pointer_Event, Wheel_Event,
Key_Event, Spatial_Test_Event>;
using Extended_Web_Event_Adapter = Basic_Web_Event_Adapter<
Extended_Web_Event, Web_Transport_Event_Decoder, Render_2D_Web_Event_Decoder,
Web_Demo_Control_Decoder, Spatial_Test_Decoder>;
std::uint32_t read_u32_le(const std::string& value, std::size_t offset) {
return static_cast<std::uint8_t>(value[offset]) |
(static_cast<std::uint32_t>(static_cast<std::uint8_t>(value[offset + 1])) << 8U) |
(static_cast<std::uint32_t>(static_cast<std::uint8_t>(value[offset + 2])) << 16U) |
(static_cast<std::uint32_t>(static_cast<std::uint8_t>(value[offset + 3])) << 24U);
}
nlohmann::json parse_json(const std::string& value) {
return nlohmann::json::parse(value);
}
Gallery_Request gallery_request(Gallery_Request_Kind kind, std::string message) {
return {kind, std::move(message)};
}
std::string open_message(std::string_view case_id, std::string_view frame_mode) {
return std::string(R"({"category":"event","type":"gallery_open","case":")") +
std::string(case_id) + R"(","frame_mode":")" + std::string(frame_mode) + R"("})";
}
nlohmann::json response_json(std::optional<Web_Response> response) {
if (!response) {
ADD_FAILURE() << "expected a Web response";
return nlohmann::json::object();
}
EXPECT_EQ(response->type, Web_Response_Type::Json);
return parse_json(response->payload);
}
nlohmann::json observe_telemetry(Gallery_Plot_Session& session) {
return response_json(session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"))).value(
"telemetry", nlohmann::json::object());
}
nlohmann::json invoke_action(Gallery_Plot_Session& session, std::string_view action_id,
std::optional<double> argument = std::nullopt) {
nlohmann::json request{{"category", "event"}, {"type", "gallery_action"},
{"action", action_id}};
if (argument)
request["argument"] = *argument;
return response_json(session.handle(gallery_request(
Gallery_Request_Kind::Action, request.dump())));
}
nlohmann::json patch_controls(Gallery_Plot_Session& session, nlohmann::json patch,
std::string_view target = "scene") {
const nlohmann::json request{{"category", "event"}, {"type", "gallery_patch"},
{"target", target}, {"patch", std::move(patch)}};
return response_json(session.handle(gallery_request(
Gallery_Request_Kind::Patch, request.dump())));
}
template <class Predicate>
bool wait_for_condition(Predicate&& predicate,
std::chrono::milliseconds timeout = std::chrono::milliseconds(750)) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
do {
if (predicate())
return true;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
} while (std::chrono::steady_clock::now() < deadline);
return predicate();
}
std::optional<Web_Response> wait_for_pixel_frame(Gallery_Plot_Session& session,
std::uint32_t width,
std::uint32_t height) {
std::optional<Web_Response> frame;
wait_for_condition([&] {
frame = session.handle(Frame_Request{});
return frame && frame->type == Web_Response_Type::Pixels &&
frame->payload.size() >= pixel_frame_header_size &&
read_u32_le(frame->payload, 4) == width &&
read_u32_le(frame->payload, 8) == height;
});
return frame;
}
void set_gallery_view_active(Gallery_Plot_Session& session, bool active) {
const auto event = Web_Event_Adapter::decode(active ?
R"({"category":"event","type":"show"})" :
R"({"category":"event","type":"hide"})");
ASSERT_TRUE(event.has_value());
EXPECT_FALSE(session.handle(*event).has_value());
}
std::uint64_t successful_render_count(Gallery_Plot_Session& session) {
return observe_telemetry(session).at("performance").at("successful_render_count")
.get<std::uint64_t>();
}
TEST(RenderiveWebBridge, DecodesOnlyEventMessages) {
const auto resize = Web_Event_Adapter::decode(
R"({"category":"event","type":"resize","width":800,"height":450})");
ASSERT_TRUE(resize.has_value());
ASSERT_TRUE(std::holds_alternative<Viewport_Resize>(*resize));
EXPECT_EQ(std::get<Viewport_Resize>(*resize).size, (Size{800, 450}));
const auto pointer = Web_Event_Adapter::decode(
R"({"category":"event","type":"pointer_press","x":22.5,"y":18,"button":"left","buttons":1,"modifiers":3})");
ASSERT_TRUE(pointer.has_value());
ASSERT_TRUE(std::holds_alternative<Pointer_Event>(*pointer));
EXPECT_EQ(std::get<Pointer_Event>(*pointer).button, Mouse_Button::Left);
EXPECT_DOUBLE_EQ(std::get<Pointer_Event>(*pointer).position.x, 22.5);
EXPECT_FALSE(Web_Event_Adapter::decode(R"({"category":"command","type":"frame"})"));
EXPECT_FALSE(Web_Event_Adapter::decode("not-json"));
const auto catalog = Web_Event_Adapter::decode(
R"({"category":"event","type":"gallery_catalog"})");
ASSERT_TRUE(catalog.has_value());
ASSERT_TRUE(std::holds_alternative<Gallery_Request>(*catalog));
EXPECT_EQ(std::get<Gallery_Request>(*catalog).kind, Gallery_Request_Kind::Catalog);
const auto open = Web_Event_Adapter::decode(
R"({"category":"event","type":"gallery_open","case":"spectrum","frame_mode":"manual"})");
ASSERT_TRUE(open.has_value());
EXPECT_EQ(std::get<Gallery_Request>(*open).kind, Gallery_Request_Kind::Open);
const auto observe = Web_Event_Adapter::decode(
R"({"category":"event","type":"gallery_observe"})");
ASSERT_TRUE(observe.has_value());
EXPECT_EQ(std::get<Gallery_Request>(*observe).kind, Gallery_Request_Kind::Observe);
const auto bounded = Web_Event_Adapter::decode(
R"({"category":"event","type":"resize","width":1e100,"height":-1e100})");
ASSERT_TRUE(bounded.has_value());
EXPECT_EQ(std::get<Viewport_Resize>(*bounded).size, (Size{1920, 180}));
}
TEST(RenderiveWebBridge, EncodesRgbaPixelFrame) {
const Pixel pixels[] = {pack_rgba(255, 0, 0), pack_rgba(0, 255, 0)};
const Image_View image{reinterpret_cast<const std::byte*>(pixels), 2, 1,
static_cast<int>(sizeof(pixels)), Pixel_Format::Premultiplied_32};
const std::string frame = encode_pixel_frame(image);
ASSERT_EQ(frame.size(), pixel_frame_header_size + 8);
EXPECT_EQ(frame.substr(0, 4), "RVP1");
EXPECT_EQ(read_u32_le(frame, 4), 2U);
EXPECT_EQ(read_u32_le(frame, 8), 1U);
EXPECT_EQ(read_u32_le(frame, 12), 8U);
const auto* rgba = reinterpret_cast<const std::uint8_t*>(frame.data() + pixel_frame_header_size);
EXPECT_EQ(rgba[0], 255);
EXPECT_EQ(rgba[1], 0);
EXPECT_EQ(rgba[2], 0);
EXPECT_EQ(rgba[3], 255);
EXPECT_EQ(rgba[4], 0);
EXPECT_EQ(rgba[5], 255);
}
TEST(RenderiveWebBridge, LegacyReceiverStillRendersPixels) {
Web_Plot_Session session;
EXPECT_FALSE(session.handle(Viewport_Resize{{640, 360}}).has_value());
EXPECT_FALSE(session.handle(Set_Demo_Mode{Demo_Mode::Usb}).has_value());
const auto frame = session.handle(Frame_Request{});
ASSERT_TRUE(frame.has_value());
EXPECT_EQ(frame->type, Web_Response_Type::Pixels);
ASSERT_GE(frame->payload.size(), pixel_frame_header_size);
EXPECT_EQ(frame->payload.substr(0, 4), "RVP1");
EXPECT_EQ(read_u32_le(frame->payload, 4), 640U);
EXPECT_EQ(read_u32_le(frame->payload, 8), 360U);
}
TEST(RenderiveWebGallery, AdminiveCatalogCoversEveryControlCaseAndThreeModes) {
const nlohmann::json catalog = parse_json(Gallery_Protocol::catalog_json());
EXPECT_EQ(catalog.at("category"), "gallery");
EXPECT_EQ(catalog.at("type"), "catalog");
EXPECT_EQ(catalog.at("protocol"), "renderive.control-gallery");
EXPECT_EQ(catalog.at("protocol_version"), 4);
EXPECT_EQ(catalog.at("case_descriptor").at("protocol"), "adminive.resource");
EXPECT_EQ(catalog.at("cases").size(), 8U);
EXPECT_EQ(catalog.at("frame_modes").size(), 3U);
EXPECT_EQ(catalog.at("coverage").at("page_count"), 3);
EXPECT_EQ(catalog.at("coverage").at("canvas_count"), 24);
EXPECT_GT(catalog.at("coverage").at("manual_control_count").get<std::size_t>(), 180U);
EXPECT_GT(catalog.at("coverage").at("manual_action_count").get<std::size_t>(), 50U);
EXPECT_EQ(catalog.at("coverage").at("frequency_modes"),
nlohmann::json::array({"spectrum", "afterglow", "sweep_spectrum"}));
EXPECT_EQ(catalog.at("coverage").at("image_interpolation_modes"),
nlohmann::json::array({"Nearest", "Bilinear", "Bicubic"}));
}
TEST(RenderiveWebGallery, CatalogOwnsDashboardFieldsAndDynamicBehavior) {
const nlohmann::json catalog = parse_json(Gallery_Protocol::catalog_json());
const auto& dashboard = catalog.at("dashboard");
EXPECT_EQ(dashboard.at("performance").at("fields").size(), 15U);
EXPECT_EQ(dashboard.at("limits").at("fields").size(), 4U);
EXPECT_EQ(dashboard.at("observer").at("sections").size(), 4U);
std::set<std::string> client_sources;
for (const auto& section : dashboard.at("observer").at("sections")) {
for (const auto& field : section.at("fields")) {
const std::string source = field.at("source").get<std::string>();
if (source.starts_with("client_performance."))
client_sources.insert(source);
}
}
constexpr std::array<std::string_view, 14> client_fields{
"display_interval_latest_ms", "display_interval_ms", "display_interval_p95_ms",
"display_jitter_ms", "frame_round_trip_ms", "transport_fps", "presentation_fps",
"websocket_buffered_bytes", "overwritten_pixel_frames", "changed_pixel_frames",
"duplicate_pixel_frames", "frame_request_timeout_count", "last_pixel_receive_age_ms",
"last_pixel_change_age_ms"
};
EXPECT_EQ(client_sources.size(), client_fields.size());
for (const auto field : client_fields)
EXPECT_TRUE(client_sources.contains("client_performance." + std::string(field))) << field;
for (const auto& mode : catalog.at("frame_modes")) {
EXPECT_TRUE(mode.contains("frame_button_label"));
EXPECT_TRUE(mode.contains("request_after_response"));
EXPECT_TRUE(mode.contains("request_on_animation_frame"));
EXPECT_TRUE(mode.contains("observer_visible"));
EXPECT_TRUE(mode.contains("accent"));
}
Gallery_Plot_Session session;
const auto contract = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "manual"))));
const auto action = std::find_if(contract.at("actions").at("data").begin(),
contract.at("actions").at("data").end(),
[](const auto& item) { return item.at("id") == "mode_render"; });
ASSERT_NE(action, contract.at("actions").at("data").end());
EXPECT_TRUE(action->at("request_frame"));
}
TEST(RenderiveWebGallery, ActionsComeFromStructiveTypeMetadata) {
using Spectrum_Schema = structive::type_descriptor_schema_t<Gallery_Spectrum>;
using Time_Axis_Schema = structive::type_descriptor_schema_t<Gallery_Time_Axis>;
using Manual_Strategy =
Manual_Refresh_Strategy<Scene2D_Frame_Data, std::mutex, Observer_State<>>;
using Manual_Schema = structive::type_descriptor_schema_t<Manual_Strategy>;
static_assert(Spectrum_Schema::template type_attribute_count<
Gallery_Action_Category> == 12);
static_assert(Time_Axis_Schema::template type_attribute_count<
Gallery_Action_Category> == 2);
static_assert(Manual_Schema::template type_attribute_count<
Gallery_Action_Category> == 5);
std::vector<Gallery_Action_Model> actions;
append_gallery_actions<Gallery_Spectrum>(actions);
EXPECT_EQ(actions.size(), 12U);
EXPECT_TRUE(std::any_of(actions.begin(), actions.end(), [](const auto& action) {
return action.id == "power_at" && action.argument_input == "number";
}));
}
TEST(RenderiveWebBridge, ComposesAdditionalEventDecoders) {
const auto event = Extended_Web_Event_Adapter::decode(
R"({"category":"event","type":"spatial_test","layer":7})");
ASSERT_TRUE(event.has_value());
ASSERT_TRUE(std::holds_alternative<Spatial_Test_Event>(*event));
EXPECT_EQ(std::get<Spatial_Test_Event>(*event).layer, 7);
}
TEST(RenderiveWebGallery, AdminiveResourcesPatchTheRealRenderableState) {
Gallery_Plot_Session session;
auto response = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "manual"))));
ASSERT_EQ(response.at("type"), "case_state");
const auto find_resource = [](const nlohmann::json& value, std::string_view target)
-> const nlohmann::json& {
const auto& resources = value.at("controls").at("resources");
const auto found = std::find_if(resources.begin(), resources.end(),
[target](const auto& resource) {
return resource.at("target").template get<std::string>() == target;
});
if (found == resources.end())
throw std::out_of_range("missing gallery resource");
return *found;
};
const auto& spectrum = find_resource(response, "spectrum");
EXPECT_EQ(spectrum.at("descriptor").at("name"), "spectrum");
EXPECT_TRUE(spectrum.at("data").at("frequency_range").is_object());
EXPECT_EQ(spectrum.at("data").at("frequency_point_size"), 512);
response = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"spectrum","patch":{"frequency_point_size":1024,"frequency_range":{"origin":90000000},"current_pen":{"color":"#ff8844"},"interpolation_mode":"Cubic_Value"}})")));
ASSERT_EQ(response.at("type"), "case_state");
const auto& updated = find_resource(response, "spectrum").at("data");
EXPECT_EQ(updated.at("frequency_point_size"), 1024);
EXPECT_EQ(updated.at("frequency_range").at("origin"), 90'000'000);
EXPECT_EQ(updated.at("current_pen").at("color"), "#ff8844");
EXPECT_EQ(updated.at("interpolation_mode"), "Cubic_Value");
const auto invalid = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"spectrum","patch":{"current_pen":{"color":"not-a-color"}}})")));
EXPECT_EQ(invalid.at("type"), "error");
const auto foreign = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"waterfall","patch":{"visible_range_only":false}})")));
EXPECT_EQ(foreign.at("type"), "error");
}
TEST(RenderiveWebGallery, EveryPublishedActionDispatchesToItsCanvasAndFrameMode) {
constexpr std::array<std::string_view, 8> cases{
"axis_lab", "spectrum", "afterglow", "sweep_spectrum",
"waterfall", "frequency_trace", "selection_overlay", "constellation"
};
constexpr std::array<std::string_view, 3> modes{"manual", "low_latency", "playback"};
std::size_t dispatched_action_instances{};
for (const auto case_id : cases) {
for (const auto mode : modes) {
Gallery_Plot_Session catalog_session;
const auto opened = response_json(catalog_session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message(case_id, mode))));
ASSERT_EQ(opened.at("type"), "case_state");
const auto actions = opened.at("actions").at("data");
std::set<std::string> ids;
for (const auto& action : actions) {
++dispatched_action_instances;
const std::string id = action.at("id");
SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode) + "/" + id);
EXPECT_TRUE(ids.insert(id).second);
EXPECT_FALSE(action.at("api").get<std::string>().empty());
EXPECT_FALSE(action.at("group").get<std::string>().empty());
Gallery_Plot_Session session;
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message(case_id, mode)))).at("type"),
"case_state");
std::optional<double> argument;
if (!action.at("argument_input").get<std::string>().empty())
argument = action.at("argument_default").get<double>();
const auto result = invoke_action(session, id, argument);
EXPECT_EQ(result.at("type"), "case_state");
EXPECT_EQ(result.at("case").at("id").get<std::string>(), case_id);
EXPECT_EQ(result.at("frame_mode").at("id").get<std::string>(), mode);
EXPECT_TRUE(result.at("telemetry").contains("kernel_observer"));
}
}
}
EXPECT_GT(dispatched_action_instances, 150U);
}
TEST(RenderiveWebGallery, CanvasSpecificActionsProduceObservableStateChanges) {
{
Gallery_Plot_Session session;
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("axis_lab", "manual"))));
const auto before = opened.at("telemetry").at("axis_lab").at("time_axis_point_count")
.get<std::size_t>();
const auto after = invoke_action(session, "append_time").at("telemetry");
EXPECT_EQ(after.at("axis_lab").at("time_axis_point_count").get<std::size_t>(),
before + 1);
EXPECT_NE(after.at("last_action_result").get<std::string>().find("tick="),
std::string::npos);
}
{
Gallery_Plot_Session session;
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "manual")))).at("type"),
"case_state");
auto telemetry = invoke_action(session, "add_line_marker", 96e6).at("telemetry");
EXPECT_EQ(telemetry.at("spectrum").at("selectable_line_markers"), 1);
telemetry = invoke_action(session, "select_marker", 0).at("telemetry");
EXPECT_EQ(telemetry.at("spectrum").at("selected_marker_index"), 0);
telemetry = invoke_action(session, "set_marker_frequency", 99e6).at("telemetry");
EXPECT_DOUBLE_EQ(telemetry.at("spectrum").at("selected_marker_frequency"), 99e6);
telemetry = invoke_action(session, "clear_markers").at("telemetry");
EXPECT_EQ(telemetry.at("spectrum").at("selectable_line_markers"), 0);
EXPECT_EQ(telemetry.at("spectrum").at("selected_marker_index"), -1);
}
{
Gallery_Plot_Session session;
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("waterfall", "manual"))));
const auto before_rows = opened.at("telemetry").at("waterfall").at("row_count")
.get<std::size_t>();
auto telemetry = invoke_action(session, "append_row").at("telemetry");
EXPECT_EQ(telemetry.at("waterfall").at("row_count").get<std::size_t>(),
before_rows + 1);
telemetry = invoke_action(session, "append_tick_row").at("telemetry");
EXPECT_EQ(telemetry.at("waterfall").at("row_count").get<std::size_t>(),
before_rows + 2);
}
{
Gallery_Plot_Session session;
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("afterglow", "manual"))));
const auto before = opened.at("telemetry").at("afterglow")
.at("history_frame_count").get<std::size_t>();
const auto telemetry = invoke_action(session, "append_spectrum").at("telemetry");
EXPECT_EQ(telemetry.at("afterglow").at("history_frame_count").get<std::size_t>(),
before + 1);
}
{
Gallery_Plot_Session session;
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("sweep_spectrum", "manual"))));
const auto before = opened.at("telemetry").at("sweep_spectrum")
.at("stored_block_count").get<std::size_t>();
const auto telemetry = invoke_action(session, "append_block").at("telemetry");
EXPECT_EQ(telemetry.at("sweep_spectrum").at("stored_block_count").get<std::size_t>(),
before + 1);
}
{
Gallery_Plot_Session session;
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("frequency_trace", "manual"))));
const auto before = opened.at("telemetry").at("frequency_trace")
.at("sample_count").get<std::size_t>();
auto telemetry = invoke_action(session, "append_sample").at("telemetry");
EXPECT_EQ(telemetry.at("frequency_trace").at("sample_count").get<std::size_t>(),
before + 1);
telemetry = invoke_action(session, "append_tick_sample").at("telemetry");
EXPECT_EQ(telemetry.at("frequency_trace").at("sample_count").get<std::size_t>(),
before + 2);
}
{
Gallery_Plot_Session session;
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("constellation", "manual"))));
const auto before = opened.at("telemetry").at("constellation")
.at("point_count").get<std::size_t>();
const auto telemetry = invoke_action(session, "append_points").at("telemetry");
EXPECT_EQ(telemetry.at("constellation").at("point_count").get<std::size_t>(),
before + 24);
}
}
TEST(RenderiveWebGallery, CommonControlsAndViewLifecycleProduceObservableEffects) {
Gallery_Plot_Session session;
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "manual")))).at("type"),
"case_state");
const auto background_state = patch_controls(session, {{"background_color", "#123456"}});
ASSERT_EQ(background_state.at("type"), "case_state") << background_state.dump();
EXPECT_EQ(background_state.at("controls").at("resources").at(0).at("data")
.at("background_color"), "#123456");
ASSERT_EQ(invoke_action(session, "mode_cycle").at("type"), "case_state");
const auto background_frame = session.handle(Frame_Request{});
ASSERT_TRUE(background_frame.has_value());
ASSERT_EQ(background_frame->type, Web_Response_Type::Pixels);
ASSERT_GE(background_frame->payload.size(), pixel_frame_header_size + 4);
const auto* corner = reinterpret_cast<const std::uint8_t*>(
background_frame->payload.data() + pixel_frame_header_size);
EXPECT_EQ(corner[0], 0x12);
EXPECT_EQ(corner[1], 0x34);
EXPECT_EQ(corner[2], 0x56);
EXPECT_EQ(corner[3], 0xff);
auto state = patch_controls(session, {
{"performance_overlay", true},
{"renderable_visible", false},
{"cache_mode", "Direct"},
{"object_name", "Observable_Spectrum"}
});
auto telemetry = state.at("telemetry");
EXPECT_TRUE(telemetry.at("performance_overlay_enabled").get<bool>());
EXPECT_FALSE(telemetry.at("renderable").at("visible").get<bool>());
EXPECT_EQ(telemetry.at("renderable").at("cache_mode"), "Direct");
EXPECT_EQ(telemetry.at("renderable").at("object_name"), "Observable_Spectrum");
state = invoke_action(session, "toggle_view");
EXPECT_FALSE(state.at("telemetry").at("view_active").get<bool>());
EXPECT_FALSE(session.handle(Frame_Request{}).has_value());
state = invoke_action(session, "toggle_view");
EXPECT_TRUE(state.at("telemetry").at("view_active").get<bool>());
ASSERT_EQ(invoke_action(session, "mode_cycle").at("type"), "case_state");
EXPECT_TRUE(session.handle(Frame_Request{}).has_value());
}
TEST(RenderiveWebGallery, SelectionOverlayPointerLifecycleCreatesAndClearsRegion) {
Gallery_Plot_Session session;
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("selection_overlay", "manual")))).at("type"),
"case_state");
const auto resize = Web_Event_Adapter::decode(
R"({"category":"event","type":"resize","width":560,"height":320})");
ASSERT_TRUE(resize.has_value());
EXPECT_FALSE(session.handle(*resize).has_value());
constexpr std::array<std::string_view, 3> drag{
R"({"category":"event","type":"pointer_press","x":120,"y":90,"button":"left","buttons":1,"modifiers":0})",
R"({"category":"event","type":"pointer_move","x":300,"y":210,"button":"none","buttons":1,"modifiers":0})",
R"({"category":"event","type":"pointer_release","x":300,"y":210,"button":"left","buttons":0,"modifiers":0})"
};
for (const auto encoded : drag) {
const auto event = Web_Event_Adapter::decode(encoded);
ASSERT_TRUE(event.has_value());
EXPECT_FALSE(session.handle(*event).has_value());
}
EXPECT_EQ(observe_telemetry(session).at("selection_regions"), 1);
EXPECT_EQ(invoke_action(session, "clear_selection").at("telemetry")
.at("selection_regions"), 0);
}
TEST(RenderiveWebGallery, EveryCanvasAcceptsTheCompleteWebEventSetAndStillRendersPixels) {
constexpr std::array<std::string_view, 8> cases{
"axis_lab", "spectrum", "afterglow", "sweep_spectrum",
"waterfall", "frequency_trace", "selection_overlay", "constellation"
};
constexpr std::array<std::string_view, 3> modes{"manual", "low_latency", "playback"};
constexpr std::array<std::string_view, 10> interactive_events{
R"({"category":"event","type":"show"})",
R"({"category":"event","type":"pointer_move","x":100,"y":100,"button":"none","buttons":0,"modifiers":0})",
R"({"category":"event","type":"pointer_press","x":100,"y":100,"button":"left","buttons":1,"modifiers":1})",
R"({"category":"event","type":"pointer_move","x":180,"y":160,"button":"none","buttons":1,"modifiers":1})",
R"({"category":"event","type":"pointer_release","x":180,"y":160,"button":"left","buttons":0,"modifiers":1})",
R"({"category":"event","type":"wheel","x":160,"y":120,"pixelDeltaX":0,"pixelDeltaY":-24,"angleDeltaX":0,"angleDeltaY":192,"buttons":0,"modifiers":2})",
R"({"category":"event","type":"key_press","key":"Escape","nativeKey":27,"repeat":false,"modifiers":0})",
R"({"category":"event","type":"key_release","key":"Escape","nativeKey":27,"repeat":false,"modifiers":0})",
R"({"category":"event","type":"leave"})",
R"({"category":"event","type":"hide"})"
};
for (const auto case_id : cases) {
for (const auto mode : modes) {
SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode));
Gallery_Plot_Session session(mode == "low_latency");
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message(case_id, mode)))).at("type"),
"case_state");
const auto resize = Web_Event_Adapter::decode(
R"({"category":"event","type":"resize","width":420,"height":260})");
ASSERT_TRUE(resize.has_value());
EXPECT_FALSE(session.handle(*resize).has_value());
for (const auto encoded : interactive_events) {
const auto event = Web_Event_Adapter::decode(encoded);
ASSERT_TRUE(event.has_value()) << encoded;
EXPECT_FALSE(session.handle(*event).has_value()) << encoded;
}
EXPECT_FALSE(session.handle(Frame_Request{}).has_value());
const auto show = Web_Event_Adapter::decode(
R"({"category":"event","type":"show"})");
ASSERT_TRUE(show.has_value());
EXPECT_FALSE(session.handle(*show).has_value());
const auto frame = wait_for_pixel_frame(session, 420, 260);
ASSERT_TRUE(frame.has_value());
ASSERT_EQ(frame->type, Web_Response_Type::Pixels);
EXPECT_EQ(frame->payload.substr(0, 4), "RVP1");
EXPECT_EQ(read_u32_le(frame->payload, 4), 420U);
EXPECT_EQ(read_u32_le(frame->payload, 8), 260U);
const auto telemetry = observe_telemetry(session);
EXPECT_TRUE(telemetry.at("view_active").get<bool>());
EXPECT_EQ(telemetry.at("viewport").at("width"), 420);
EXPECT_EQ(telemetry.at("viewport").at("height"), 260);
}
}
}
TEST(RenderiveWebGallery, InvalidInteractiveEventsAreRejectedBeforeReachingCore2) {
constexpr std::array<std::string_view, 8> invalid{
R"({"category":"command","type":"pointer_move","x":1,"y":2})",
R"({"category":"event","type":"pointer_move","x":1})",
R"({"category":"event","type":"pointer_press","x":"bad","y":2})",
R"({"category":"event","type":"wheel","x":1,"y":2})",
R"({"category":"event","type":"resize","width":"bad","height":200})",
R"({"category":"event","type":"control","control":"mode","value":"INVALID"})",
R"({"category":"event","type":"unknown"})",
"not-json"
};
for (const auto encoded : invalid)
EXPECT_FALSE(Web_Event_Adapter::decode(encoded).has_value()) << encoded;
}
TEST(RenderiveWebGallery, EveryIndependentCore2CanvasBuildsAndRenders) {
constexpr std::array<std::string_view, 8> cases{
"axis_lab", "spectrum", "afterglow", "sweep_spectrum",
"waterfall", "frequency_trace", "selection_overlay", "constellation"
};
constexpr std::array<std::string_view, 3> modes{"manual", "low_latency", "playback"};
for (const std::string_view mode : modes) {
for (const std::string_view case_id : cases) {
Gallery_Plot_Session session(mode == "low_latency");
const auto opened = session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message(case_id, mode)));
ASSERT_TRUE(opened.has_value()) << case_id;
ASSERT_EQ(opened->type, Web_Response_Type::Json) << case_id;
const auto state = parse_json(opened->payload);
EXPECT_EQ(state.at("type"), "case_state") << case_id;
EXPECT_EQ(state.at("case").at("id").get<std::string>(), std::string(case_id))
<< case_id;
EXPECT_EQ(state.at("frame_mode").at("id"), std::string(mode)) << case_id;
const auto& resources = state.at("controls").at("resources");
ASSERT_FALSE(resources.empty()) << case_id;
for (const auto& resource : resources) {
EXPECT_EQ(resource.at("descriptor").at("protocol"),
"adminive.resource") << case_id;
EXPECT_EQ(resource.at("view").at("protocol"),
"adminive.view") << case_id;
}
EXPECT_FALSE(session.handle(Viewport_Resize{{480, 280}}).has_value()) << case_id;
const auto frame = wait_for_pixel_frame(session, 480, 280);
ASSERT_TRUE(frame.has_value()) << case_id;
ASSERT_EQ(frame->type, Web_Response_Type::Pixels) << case_id;
EXPECT_EQ(frame->payload.substr(0, 4), "RVP1") << case_id;
EXPECT_EQ(read_u32_le(frame->payload, 4), 480U) << case_id;
EXPECT_EQ(read_u32_le(frame->payload, 8), 280U) << case_id;
const auto observed = session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"));
ASSERT_TRUE(observed.has_value()) << case_id;
const auto telemetry = parse_json(observed->payload);
EXPECT_EQ(telemetry.at("type"), "observer_state") << case_id;
EXPECT_EQ(telemetry.at("frame_mode").at("id"), std::string(mode)) << case_id;
EXPECT_TRUE(telemetry.at("telemetry").contains("kernel_observer")) << case_id;
EXPECT_TRUE(telemetry.at("telemetry").contains("performance")) << case_id;
EXPECT_TRUE(telemetry.at("telemetry").contains("data_shape")) << case_id;
EXPECT_GT(telemetry.at("telemetry").at("data_shape").at("rendered_elements")
.get<std::size_t>(), 0U) << case_id;
}
}
}
TEST(RenderiveWebGallery, ActionsMutateLiveCore2ObjectsAndReturnTelemetry) {
Gallery_Plot_Session session;
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Open,
open_message("spectrum", "low_latency"))));
const auto add = session.handle(gallery_request(
Gallery_Request_Kind::Action,
R"({"category":"event","type":"gallery_action","action":"add_line_marker","argument":97000000})"));
ASSERT_TRUE(add.has_value());
const auto add_state = parse_json(add->payload);
EXPECT_EQ(add_state.at("type"), "case_state");
EXPECT_EQ(add_state.at("telemetry").at("spectrum").at("selectable_line_markers"), 1);
const auto patch = session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"spectrum","patch":{"interpolation_mode":"Step_Left","max_hold_visible":true}})"));
ASSERT_TRUE(patch.has_value());
EXPECT_EQ(parse_json(patch->payload).at("type"), "case_state");
}
TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers) {
const auto action = [](Gallery_Plot_Session& session, std::string_view id,
std::optional<int> argument = std::nullopt) {
nlohmann::json request{{"category", "event"}, {"type", "gallery_action"},
{"action", id}};
if (argument)
request["argument"] = *argument;
return session.handle(gallery_request(Gallery_Request_Kind::Action, request.dump()));
};
const auto observe = [](Gallery_Plot_Session& session) {
const auto response = session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"));
EXPECT_TRUE(response.has_value());
return parse_json(response->payload).at("telemetry");
};
Gallery_Plot_Session manual;
ASSERT_TRUE(manual.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "manual"))));
ASSERT_TRUE(action(manual, "mode_prepare"));
auto manual_observer = observe(manual).at("kernel_observer");
EXPECT_EQ(manual_observer.at("last_event"), "prepared");
EXPECT_EQ(manual_observer.at("pending_frame_count"), 1);
ASSERT_TRUE(action(manual, "mode_refresh"));
ASSERT_TRUE(action(manual, "mode_render"));
manual_observer = observe(manual).at("kernel_observer");
EXPECT_EQ(manual_observer.at("last_event"), "rendered");
EXPECT_GE(manual_observer.at("consumed_frame_count").get<std::uint64_t>(), 2U);
Gallery_Plot_Session low_latency;
ASSERT_TRUE(low_latency.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))));
const auto high_frequency = low_latency.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":1000000000}})"));
ASSERT_TRUE(high_frequency.has_value());
ASSERT_EQ(parse_json(high_frequency->payload).at("type"), "case_state");
ASSERT_TRUE(low_latency.handle(Frame_Request{}));
const auto low_telemetry = observe(low_latency);
EXPECT_EQ(low_telemetry.at("frame_mode"), "low_latency");
EXPECT_GT(low_telemetry.at("kernel_observer").at("observation_count").get<std::uint64_t>(), 0U);
EXPECT_TRUE(low_telemetry.contains("render_fps"));
ASSERT_TRUE(low_telemetry.contains("low_latency_limit"));
const auto& limit = low_telemetry.at("low_latency_limit");
EXPECT_FALSE(limit.at("consumer_limited").get<bool>());
EXPECT_FALSE(limit.at("frequency_limited").get<bool>());
EXPECT_TRUE(limit.at("paint_limited").get<bool>() || limit.at("render_limited").get<bool>());
EXPECT_EQ(limit.at("target_interval_ns"), 1);
EXPECT_EQ(limit.at("consumer_interval_ns"), 0);
EXPECT_GT(limit.at("bottleneck_duration_ns").get<std::uint64_t>(), 1U);
EXPECT_TRUE(low_telemetry.at("kernel_observer").contains("paint_lease_wait_ns"));
EXPECT_TRUE(low_telemetry.at("kernel_observer").contains("render_finish_state_wait_ns"));
constexpr std::array<std::string_view, 33> observer_fields{
"mode", "last_event", "limit_state", "configured_frequency_hz", "observation_count",
"produced_frame_count", "consumed_frame_count", "dropped_frame_count",
"failed_operation_count", "pending_frame_count", "latest_sequence",
"paint_duration_ns", "render_duration_ns", "target_interval_ns", "frequency_limit_enabled",
"consumer_feedback_enabled", "bottleneck_duration_ns", "consumer_sample_interval_ns",
"consumer_smoothed_interval_ns", "consumer_variation_ns", "consumer_safety_interval_ns", "consumer_interval_ns",
"next_refresh_interval_ns", "paint_lease_wait_ns",
"paint_state_wait_ns", "publish_state_wait_ns", "ready_wait_ns",
"frame_age_at_render_ns", "render_lease_wait_ns", "render_state_wait_ns",
"render_finish_state_wait_ns", "queue_wait_ns", "end_to_end_ns"
};
for (const auto field : observer_fields)
EXPECT_TRUE(low_telemetry.at("kernel_observer").contains(field)) << field;
Gallery_Plot_Session playback;
ASSERT_TRUE(playback.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "playback"))));
ASSERT_TRUE(action(playback, "mode_enqueue_burst", 5));
auto playback_observer = observe(playback).at("kernel_observer");
EXPECT_EQ(playback_observer.at("last_event"), "enqueued");
EXPECT_EQ(playback_observer.at("pending_frame_count"), 5);
ASSERT_TRUE(action(playback, "mode_dequeue"));
playback_observer = observe(playback).at("kernel_observer");
EXPECT_EQ(playback_observer.at("last_event"), "rendered");
EXPECT_EQ(playback_observer.at("pending_frame_count"), 4);
EXPECT_GT(playback_observer.at("queue_wait_ns").get<std::uint64_t>(), 0U);
}
TEST(RenderiveWebGallery, ManualStrategyRunsOnlyExplicitPrepareRefreshRenderCycles) {
Gallery_Plot_Session session(true);
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "manual"))));
ASSERT_EQ(opened.at("type"), "case_state");
const auto baseline = opened.at("telemetry").at("kernel_observer");
EXPECT_EQ(baseline.at("mode"), "manual");
EXPECT_EQ(baseline.at("limit_state"), "not_applicable");
std::this_thread::sleep_for(std::chrono::milliseconds(80));
auto observer = observe_telemetry(session).at("kernel_observer");
EXPECT_EQ(observer.at("latest_sequence"), baseline.at("latest_sequence"));
EXPECT_EQ(observer.at("produced_frame_count"), baseline.at("produced_frame_count"));
EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count"));
auto state = invoke_action(session, "mode_prepare");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "prepared");
EXPECT_EQ(observer.at("pending_frame_count"), 1);
EXPECT_EQ(observer.at("produced_frame_count").get<std::uint64_t>(),
baseline.at("produced_frame_count").get<std::uint64_t>() + 1);
state = invoke_action(session, "mode_refresh");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "refresh_succeeded");
EXPECT_EQ(observer.at("pending_frame_count"), 0);
EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count"));
state = invoke_action(session, "mode_render");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "rendered");
EXPECT_EQ(observer.at("pending_frame_count"), 0);
EXPECT_EQ(observer.at("consumed_frame_count").get<std::uint64_t>(),
baseline.at("consumed_frame_count").get<std::uint64_t>() + 1);
ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state");
state = invoke_action(session, "mode_discard");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "manually_discarded");
EXPECT_EQ(observer.at("pending_frame_count"), 0);
EXPECT_EQ(observer.at("dropped_frame_count").get<std::uint64_t>(),
baseline.at("dropped_frame_count").get<std::uint64_t>() + 1);
const auto failed_before = observer.at("failed_operation_count").get<std::uint64_t>();
state = invoke_action(session, "mode_refresh");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "refresh_failed");
EXPECT_EQ(observer.at("failed_operation_count").get<std::uint64_t>(), failed_before + 1);
const auto sequence_after_actions = observer.at("latest_sequence");
std::this_thread::sleep_for(std::chrono::milliseconds(80));
EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("latest_sequence"),
sequence_after_actions);
}
TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggressiveFrequency) {
Gallery_Plot_Session session(true);
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"),
"case_state");
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":1000}})"))).at("type"),
"case_state");
ASSERT_TRUE(wait_for_condition([&session] {
return successful_render_count(session) >= 4;
}));
std::string first_pixels;
std::string last_pixels;
for (int iteration = 0; iteration < 12; ++iteration) {
const auto started = std::chrono::steady_clock::now();
const auto frame = session.handle(Frame_Request{});
ASSERT_TRUE(frame.has_value()) << iteration;
ASSERT_EQ(frame->type, Web_Response_Type::Pixels) << iteration;
const auto telemetry = observe_telemetry(session);
EXPECT_LT(std::chrono::steady_clock::now() - started,
std::chrono::milliseconds(250)) << iteration;
EXPECT_EQ(telemetry.at("kernel_observer").at("configured_frequency_hz"), 1000.0);
if (iteration == 0)
first_pixels = frame->payload;
last_pixels = frame->payload;
std::this_thread::sleep_for(std::chrono::milliseconds(3));
}
EXPECT_NE(first_pixels, last_pixels);
const auto telemetry = observe_telemetry(session);
const auto& observer = telemetry.at("kernel_observer");
EXPECT_EQ(observer.at("target_interval_ns"), 1'000'000);
EXPECT_GT(observer.at("latest_sequence").get<std::uint64_t>(), 4U);
EXPECT_GT(telemetry.at("performance").at("successful_render_count").get<std::uint64_t>(), 4U);
EXPECT_TRUE(observer.at("limit_state") == "frequency_limited" ||
observer.at("limit_state") == "paint_limited" ||
observer.at("limit_state") == "render_limited" ||
observer.at("limit_state") == "consumer_limited");
}
TEST(RenderiveWebGallery, FrequencyLimitCannotBeBypassedByForegroundRescheduleEvents) {
Gallery_Plot_Session session(true);
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"),
"case_state");
const auto patched = patch_controls(session, {
{"frequency_limit_enabled", true},
{"max_render_fps", 30.0},
{"feedback", {{"enabled", false}}}
});
ASSERT_EQ(patched.at("type"), "case_state");
const auto& patched_observer = patched.at("telemetry").at("kernel_observer");
EXPECT_TRUE(patched_observer.at("frequency_limit_enabled").get<bool>());
EXPECT_FALSE(patched_observer.at("consumer_feedback_enabled").get<bool>());
EXPECT_EQ(patched_observer.at("target_interval_ns"), 33'333'333);
ASSERT_TRUE(wait_for_condition([&session] {
return successful_render_count(session) >= 3;
}));
const auto pointer = Web_Event_Adapter::decode(
R"({"category":"event","type":"pointer_move","x":320,"y":180,"button":"none","buttons":0,"modifiers":0})");
ASSERT_TRUE(pointer.has_value());
const auto before = successful_render_count(session);
const auto started = std::chrono::steady_clock::now();
const auto deadline = started + std::chrono::milliseconds(450);
while (std::chrono::steady_clock::now() < deadline) {
EXPECT_FALSE(session.handle(*pointer).has_value());
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
const auto elapsed = std::chrono::steady_clock::now() - started;
const auto after = successful_render_count(session);
const auto rendered = after - before;
EXPECT_GT(rendered, 0U);
EXPECT_LE(rendered, 16U);
const auto telemetry = observe_telemetry(session);
const auto& observer = telemetry.at("kernel_observer");
EXPECT_EQ(observer.at("target_interval_ns"), 33'333'333);
EXPECT_GE(observer.at("next_refresh_interval_ns").get<std::uint64_t>(), 33'333'333U);
EXPECT_GE(elapsed, std::chrono::milliseconds(450));
}
TEST(RenderiveWebGallery, ConsumerFeedbackCannotBeBypassedByForegroundRescheduleEvents) {
Gallery_Plot_Session session(true);
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"),
"case_state");
ASSERT_EQ(patch_controls(session, {
{"frequency_limit_enabled", false},
{"feedback", {{"enabled", true}}}
}).at("type"), "case_state");
auto observed = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":25,"presentation_fps":25,"websocket_buffered_bytes":0,"changed_pixel_frames":1,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":5,"display_interval_ms":40,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})")));
auto observer = observed.at("telemetry").at("kernel_observer");
EXPECT_FALSE(observer.at("frequency_limit_enabled").get<bool>());
EXPECT_TRUE(observer.at("consumer_feedback_enabled").get<bool>());
EXPECT_EQ(observer.at("consumer_interval_ns"), 40'000'000);
ASSERT_TRUE(wait_for_condition([&session] {
return successful_render_count(session) >= 3;
}));
const auto pointer = Web_Event_Adapter::decode(
R"({"category":"event","type":"pointer_move","x":320,"y":180,"button":"none","buttons":0,"modifiers":0})");
ASSERT_TRUE(pointer.has_value());
const auto before = successful_render_count(session);
const auto started = std::chrono::steady_clock::now();
const auto deadline = started + std::chrono::milliseconds(450);
while (std::chrono::steady_clock::now() < deadline) {
EXPECT_FALSE(session.handle(*pointer).has_value());
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
const auto after = successful_render_count(session);
const auto rendered = after - before;
EXPECT_GT(rendered, 0U);
EXPECT_LE(rendered, 14U);
observed = observe_telemetry(session);
observer = observed.at("kernel_observer");
EXPECT_EQ(observer.at("consumer_interval_ns"), 40'000'000);
EXPECT_GE(observer.at("next_refresh_interval_ns").get<std::uint64_t>(), 40'000'000U);
}
TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedbackWithoutTransportFpsCap) {
Gallery_Plot_Session session(true);
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"),
"case_state");
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":1000000000}})"))).at("type"),
"case_state");
auto observed = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":50,"presentation_fps":50,"websocket_buffered_bytes":0,"changed_pixel_frames":10,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":80,"display_interval_ms":20,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})")));
auto observer = observed.at("telemetry").at("kernel_observer");
auto limit = observed.at("telemetry").at("low_latency_limit");
EXPECT_TRUE(observer.at("frequency_limit_enabled").get<bool>());
EXPECT_TRUE(observer.at("consumer_feedback_enabled").get<bool>());
EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 20'000'000);
EXPECT_EQ(observer.at("consumer_smoothed_interval_ns"), 20'000'000);
EXPECT_EQ(observer.at("consumer_variation_ns"), 0);
EXPECT_EQ(observer.at("consumer_safety_interval_ns"), 20'000'000);
EXPECT_EQ(observer.at("consumer_interval_ns"), 20'000'000);
EXPECT_EQ(observer.at("limit_state"), "consumer_limited");
EXPECT_EQ(limit.at("consumer_feedback_source"), "presentation");
observed = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":45,"presentation_fps":45,"websocket_buffered_bytes":0,"changed_pixel_frames":20,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":4,"display_interval_ms":22,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})")));
observer = observed.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 22'000'000);
EXPECT_EQ(observer.at("consumer_smoothed_interval_ns"), 21'000'000);
EXPECT_EQ(observer.at("consumer_variation_ns"), 125'000);
EXPECT_EQ(observer.at("consumer_safety_interval_ns"), 22'000'000);
EXPECT_EQ(observer.at("consumer_interval_ns"), 22'000'000);
observed = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"feedback":{"enabled":false},"frequency_limit_enabled":false}})")));
observer = observed.at("telemetry").at("kernel_observer");
EXPECT_FALSE(observer.at("frequency_limit_enabled").get<bool>());
EXPECT_FALSE(observer.at("consumer_feedback_enabled").get<bool>());
EXPECT_EQ(observer.at("target_interval_ns"), 0);
EXPECT_EQ(observer.at("consumer_interval_ns"), 0);
EXPECT_TRUE(observer.at("limit_state") == "paint_limited" ||
observer.at("limit_state") == "render_limited" ||
observer.at("limit_state") == "unlimited");
const auto rejected = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"pixel_stream_fps":30}})")));
EXPECT_EQ(rejected.at("type"), "error");
}
TEST(RenderiveWebGallery, ConsumerFeedbackSourcesCanBeSelectedIndependentlyAndManually) {
Gallery_Plot_Session session(true);
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"),
"case_state");
ASSERT_EQ(patch_controls(session, {
{"frequency_limit_enabled", false},
{"feedback", {{"enabled", true}, {"pixel", true},
{"presentation", false}, {"manual", false}}}
}).at("type"), "case_state");
auto observed = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":25,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":1,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":5,"display_interval_ms":16,"display_interval_latest_ms":16,"display_interval_p95_ms":17,"display_jitter_ms":1,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})")));
auto observer = observed.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("consumer_feedback_source"), "pixel");
EXPECT_EQ(observer.at("consumer_pixel_interval_ns"), 40'000'000);
EXPECT_EQ(observer.at("consumer_presentation_interval_ns"), 16'000'000);
EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 40'000'000);
observed = patch_controls(session, {
{"feedback", {{"pixel", false}, {"presentation", true}}}
});
observer = observed.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("consumer_feedback_source"), "presentation");
EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 16'000'000);
observed = patch_controls(session, {
{"feedback", {{"presentation", false}, {"manual", true},
{"manual_fps", 20.0}}}
});
observer = observed.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("consumer_feedback_source"), "manual");
EXPECT_EQ(observer.at("consumer_manual_interval_ns"), 50'000'000);
EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 50'000'000);
observed = patch_controls(session, {
{"feedback", {{"manual", false}}}
});
observer = observed.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("consumer_feedback_source"), "none");
EXPECT_FALSE(observer.at("consumer_feedback_enabled").get<bool>());
EXPECT_TRUE(observer.at("consumer_feedback_master_enabled").get<bool>());
EXPECT_EQ(observer.at("consumer_interval_ns"), 0);
}
TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndReportsEmptyQueue) {
Gallery_Plot_Session session(true);
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "playback"))));
ASSERT_EQ(opened.at("type"), "case_state");
const auto baseline = opened.at("telemetry").at("kernel_observer");
EXPECT_EQ(baseline.at("mode"), "playback");
EXPECT_EQ(baseline.at("limit_state"), "not_applicable");
std::this_thread::sleep_for(std::chrono::milliseconds(80));
auto observer = observe_telemetry(session).at("kernel_observer");
EXPECT_EQ(observer.at("latest_sequence"), baseline.at("latest_sequence"));
EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count"));
auto state = invoke_action(session, "mode_enqueue_burst", 5);
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "enqueued");
EXPECT_EQ(observer.at("pending_frame_count"), 5);
std::this_thread::sleep_for(std::chrono::milliseconds(20));
EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("pending_frame_count"), 5);
state = invoke_action(session, "mode_dequeue");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "rendered");
EXPECT_EQ(observer.at("pending_frame_count"), 4);
EXPECT_GT(observer.at("queue_wait_ns").get<std::uint64_t>(), 0U);
state = invoke_action(session, "mode_cycle");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "rendered");
EXPECT_EQ(observer.at("pending_frame_count"), 4);
for (int remaining = 3; remaining >= 0; --remaining) {
state = invoke_action(session, "mode_dequeue");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("pending_frame_count"), remaining);
}
const auto failed_before = observer.at("failed_operation_count").get<std::uint64_t>();
state = invoke_action(session, "mode_dequeue");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "queue_empty");
EXPECT_EQ(observer.at("failed_operation_count").get<std::uint64_t>(), failed_before + 1);
EXPECT_EQ(observer.at("pending_frame_count"), 0);
const auto sequence_after_actions = observer.at("latest_sequence");
std::this_thread::sleep_for(std::chrono::milliseconds(80));
EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("latest_sequence"),
sequence_after_actions);
}
TEST(RenderiveWebGallery, BackendRejectsActionsHiddenByCurrentCaseOrFrameMode) {
struct Rejected_Action {
std::string_view mode;
std::string_view action;
};
constexpr std::array rejected{
Rejected_Action{"manual", "mode_enqueue"},
Rejected_Action{"low_latency", "mode_refresh"},
Rejected_Action{"playback", "mode_discard"},
Rejected_Action{"manual", "append_row"}
};
for (const auto& item : rejected) {
Gallery_Plot_Session session;
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", item.mode)))).at("type"),
"case_state") << item.mode;
const auto response = invoke_action(session, item.action);
EXPECT_EQ(response.at("type"), "error") << item.mode << '/' << item.action;
EXPECT_TRUE(response.at("field_errors").contains("action"))
<< item.mode << '/' << item.action;
}
}
TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) {
Gallery_Plot_Session session(true);
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))));
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":120}})")));
ASSERT_TRUE(wait_for_condition([&session] {
return successful_render_count(session) >= 4;
}));
const auto observed = session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":120,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":17,"duplicate_pixel_frames":3,"frame_request_timeout_count":2,"frame_round_trip_ms":4.5,"display_interval_ms":16.7,"display_interval_latest_ms":16.9,"display_interval_p95_ms":17.4,"display_jitter_ms":0.7,"overwritten_pixel_frames":5,"last_pixel_receive_age_ms":8.5,"last_pixel_change_age_ms":12.5}})"));
ASSERT_TRUE(observed.has_value());
const auto telemetry = parse_json(observed->payload).at("telemetry");
const auto& performance = telemetry.at("performance");
EXPECT_TRUE(performance.at("automatic_low_latency_scheduler").get<bool>());
EXPECT_GE(performance.at("successful_render_count").get<std::uint64_t>(), 4U);
EXPECT_GT(performance.at("measured_fps").get<double>(), 0.0);
EXPECT_DOUBLE_EQ(telemetry.at("kernel_observer").at("configured_frequency_hz"), 120.0);
EXPECT_EQ(telemetry.at("kernel_observer").at("target_interval_ns"), 8'333'333);
EXPECT_TRUE(telemetry.at("kernel_observer").at("limit_state") == "frequency_limited" ||
telemetry.at("kernel_observer").at("limit_state") == "paint_limited" ||
telemetry.at("kernel_observer").at("limit_state") == "render_limited" ||
telemetry.at("kernel_observer").at("limit_state") == "consumer_limited");
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("transport_fps"), 120.0);
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("presentation_fps"), 60.0);
EXPECT_EQ(telemetry.at("client_performance").at("changed_pixel_frames"), 17);
EXPECT_EQ(telemetry.at("client_performance").at("duplicate_pixel_frames"), 3);
EXPECT_EQ(telemetry.at("client_performance").at("frame_request_timeout_count"), 2);
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("frame_round_trip_ms"), 4.5);
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_interval_ms"), 16.7);
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_interval_latest_ms"), 16.9);
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_interval_p95_ms"), 17.4);
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_jitter_ms"), 0.7);
EXPECT_EQ(telemetry.at("client_performance").at("overwritten_pixel_frames"), 5);
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("last_pixel_receive_age_ms"), 8.5);
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("last_pixel_change_age_ms"), 12.5);
}
TEST(RenderiveWebGallery, ProductionLowLatencyStopsWhileHiddenAndResumes) {
Gallery_Plot_Session session(true);
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))));
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":120}})")));
ASSERT_TRUE(wait_for_condition([&session] {
return successful_render_count(session) >= 3;
}));
set_gallery_view_active(session, false);
const auto stopped_count = successful_render_count(session);
EXPECT_FALSE(session.handle(Frame_Request{}).has_value());
std::this_thread::sleep_for(std::chrono::milliseconds(80));
EXPECT_EQ(successful_render_count(session), stopped_count);
set_gallery_view_active(session, true);
EXPECT_TRUE(wait_for_condition([&session, stopped_count] {
return successful_render_count(session) > stopped_count;
}));
EXPECT_TRUE(session.handle(Frame_Request{}).has_value());
}
TEST(RenderiveWebGallery, ProductionLowLatencyOnlyRendersActiveSessions) {
constexpr std::size_t session_count = 8;
std::vector<std::unique_ptr<Gallery_Plot_Session>> sessions;
sessions.reserve(session_count);
for (std::size_t index = 0; index < session_count; ++index) {
auto session = std::make_unique<Gallery_Plot_Session>(true);
ASSERT_TRUE(session->handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))));
ASSERT_TRUE(session->handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":60}})")));
if (index >= 2)
set_gallery_view_active(*session, false);
sessions.push_back(std::move(session));
}
for (std::size_t index = 0; index < 2; ++index) {
ASSERT_TRUE(wait_for_condition([&sessions, index] {
return successful_render_count(*sessions[index]) >= 2;
}));
}
std::array<std::uint64_t, session_count> hidden_counts{};
for (std::size_t index = 2; index < session_count; ++index)
hidden_counts[index] = successful_render_count(*sessions[index]);
std::this_thread::sleep_for(std::chrono::milliseconds(80));
for (std::size_t index = 2; index < session_count; ++index)
EXPECT_EQ(successful_render_count(*sessions[index]), hidden_counts[index]) << index;
set_gallery_view_active(*sessions[0], false);
set_gallery_view_active(*sessions[1], false);
const auto first_stopped = successful_render_count(*sessions[0]);
const auto second_stopped = successful_render_count(*sessions[1]);
set_gallery_view_active(*sessions[2], true);
set_gallery_view_active(*sessions[3], true);
EXPECT_TRUE(wait_for_condition([&sessions, baseline = hidden_counts[2]] {
return successful_render_count(*sessions[2]) > baseline;
}));
EXPECT_TRUE(wait_for_condition([&sessions, baseline = hidden_counts[3]] {
return successful_render_count(*sessions[3]) > baseline;
}));
std::this_thread::sleep_for(std::chrono::milliseconds(80));
EXPECT_EQ(successful_render_count(*sessions[0]), first_stopped);
EXPECT_EQ(successful_render_count(*sessions[1]), second_stopped);
}
void expect_low_latency_canvas_to_change(std::string_view case_id) {
SCOPED_TRACE(std::string(case_id));
Gallery_Plot_Session session(true);
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message(case_id, "low_latency"))));
ASSERT_FALSE(session.handle(Viewport_Resize{{240, 180}}).has_value());
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":120}})")));
std::this_thread::sleep_for(std::chrono::milliseconds(90));
const auto first = session.handle(Frame_Request{});
ASSERT_TRUE(first.has_value());
ASSERT_EQ(first->type, Web_Response_Type::Pixels);
std::this_thread::sleep_for(std::chrono::milliseconds(90));
const auto second = session.handle(Frame_Request{});
ASSERT_TRUE(second.has_value());
ASSERT_EQ(second->type, Web_Response_Type::Pixels);
EXPECT_NE(first->payload, second->payload);
const auto observed = session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"));
ASSERT_TRUE(observed.has_value());
const auto telemetry = parse_json(observed->payload).at("telemetry");
EXPECT_GT(telemetry.at("frame_index").get<std::uint64_t>(), 2U);
EXPECT_GT(telemetry.at("kernel_observer").at("latest_sequence").get<std::uint64_t>(), 1U);
}
TEST(RenderiveWebGallery, AxisLabLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("axis_lab");
}
TEST(RenderiveWebGallery, SpectrumLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("spectrum");
}
TEST(RenderiveWebGallery, AfterglowLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("afterglow");
}
TEST(RenderiveWebGallery, SweepSpectrumLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("sweep_spectrum");
}
TEST(RenderiveWebGallery, WaterfallLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("waterfall");
}
TEST(RenderiveWebGallery, FrequencyTraceLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("frequency_trace");
}
TEST(RenderiveWebGallery, SelectionOverlayLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("selection_overlay");
}
TEST(RenderiveWebGallery, ConstellationLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("constellation");
}
TEST(RenderiveWebGallery, DirectPropertyChangesAndAllImageModesRender) {
Gallery_Plot_Session constellation;
ASSERT_TRUE(constellation.handle(gallery_request(
Gallery_Request_Kind::Open,
open_message("constellation", "low_latency"))));
const auto patched = constellation.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"constellation","patch":{"type":"PSK16","phase_offset_radians":0.75}})"));
ASSERT_TRUE(patched.has_value());
const auto patched_json = parse_json(patched->payload);
EXPECT_EQ(patched_json.at("controls").at("resources").at(3).at("data").at("type"),
"PSK16");
ASSERT_TRUE(constellation.handle(Frame_Request{}).has_value());
Gallery_Plot_Session waterfall;
ASSERT_TRUE(waterfall.handle(gallery_request(
Gallery_Request_Kind::Open,
open_message("waterfall", "low_latency"))));
for (const std::string_view mode : {"Nearest", "Bilinear", "Bicubic"}) {
const std::string patch =
std::string(R"({"category":"event","type":"gallery_patch","target":"waterfall","patch":{"interpolation_mode":")") +
std::string(mode) + R"("}})";
ASSERT_TRUE(waterfall.handle(gallery_request(Gallery_Request_Kind::Patch, patch)).has_value())
<< mode;
ASSERT_TRUE(waterfall.handle(Frame_Request{}).has_value()) << mode;
}
const auto palette = waterfall.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","target":"waterfall","patch":{"color_map":"Ember"}})"));
ASSERT_TRUE(palette.has_value());
ASSERT_TRUE(waterfall.handle(Frame_Request{}).has_value());
const auto waterfall_observer = waterfall.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"));
ASSERT_TRUE(waterfall_observer.has_value());
const auto waterfall_telemetry = parse_json(waterfall_observer->payload).at("telemetry");
EXPECT_GT(waterfall_telemetry.at("waterfall").at("row_count").get<std::size_t>(), 0U);
EXPECT_GT(waterfall_telemetry.at("waterfall").at("stored_point_count").get<std::size_t>(), 0U);
EXPECT_GT(waterfall_telemetry.at("waterfall").at("rendered_cell_count").get<std::size_t>(), 0U);
}
} // namespace
} // namespace renderive::web