帧颜色格式优化
This commit is contained in:
@@ -1,14 +1,68 @@
|
||||
#include "Frame_2D.hpp"
|
||||
#include <blend2d/core/pixelconverter.h>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
namespace aethera::render_2d {
|
||||
namespace {
|
||||
const BLPixelConverter& rgba_converter() {
|
||||
static const BLPixelConverter converter = [] {
|
||||
BLFormatInfo source{};
|
||||
if (source.query(BL_FORMAT_PRGB32) != BL_SUCCESS)
|
||||
throw std::runtime_error("failed to query Blend2D PRGB32 format");
|
||||
constexpr std::uint8_t sizes[4]{8, 8, 8, 8};
|
||||
constexpr std::uint8_t shifts[4]{0, 8, 16, 24};
|
||||
BLFormatInfo destination{};
|
||||
destination.init(32, BL_FORMAT_FLAG_RGBA, sizes, shifts);
|
||||
if (destination.sanitize() != BL_SUCCESS)
|
||||
throw std::runtime_error("failed to describe browser RGBA8 format");
|
||||
BLPixelConverter result;
|
||||
if (result.create(destination, source) != BL_SUCCESS)
|
||||
throw std::runtime_error("failed to create Blend2D RGBA8 converter");
|
||||
return result;
|
||||
}();
|
||||
return converter;
|
||||
}
|
||||
}
|
||||
struct Frame_2D::Private {
|
||||
Blend2D_Cache color{}; /* Scene 直接合成且由外部 Frame 生命周期持有的最终颜色层。 */
|
||||
Blend2D_Cache color{}; /* Scene 直接合成的原生 PRGB32 颜色层。 */
|
||||
Pixel_Format output_format{native_pixel_format}; /* 调用方为本帧选择的最终协议格式。 */
|
||||
};
|
||||
Frame_2D::Frame_2D(Frame_Identity identity) : Render_Frame(identity), d(std::make_unique<Private>()) {}
|
||||
Frame_2D::Frame_2D(Frame_Identity identity, Pixel_Format output_format)
|
||||
: Render_Frame(identity), d(std::make_unique<Private>()) {
|
||||
if (std::ranges::find(supported_pixel_formats, output_format) ==
|
||||
supported_pixel_formats.end())
|
||||
throw std::invalid_argument("Frame_2D output pixel format is unsupported");
|
||||
d->output_format = output_format;
|
||||
}
|
||||
Frame_2D::~Frame_2D() = default;
|
||||
Pixel_Format Frame_2D::output_format() const noexcept {
|
||||
return d->output_format;
|
||||
}
|
||||
Image_View Frame_2D::image() const { return d->color.view(); }
|
||||
Pixel_Buffer Frame_2D::output_pixels() const {
|
||||
const Image_View source = image();
|
||||
Pixel_Buffer result{{}, source.width, source.height, d->output_format};
|
||||
if (source.empty()) return result;
|
||||
const std::size_t row_size = static_cast<std::size_t>(source.width) * 4U;
|
||||
result.bytes.resize(row_size * static_cast<std::size_t>(source.height));
|
||||
if (d->output_format == native_pixel_format) {
|
||||
for (int y = 0; y < source.height; ++y)
|
||||
std::memcpy(result.bytes.data() + static_cast<std::size_t>(y) * row_size,
|
||||
source.data + static_cast<std::ptrdiff_t>(y) * source.stride,
|
||||
row_size);
|
||||
return result;
|
||||
}
|
||||
if (rgba_converter().convert_rect(
|
||||
result.bytes.data(), static_cast<intptr_t>(row_size), source.data,
|
||||
source.stride, static_cast<std::uint32_t>(source.width),
|
||||
static_cast<std::uint32_t>(source.height)) != BL_SUCCESS)
|
||||
throw std::runtime_error("Blend2D failed to convert PRGB32 to RGBA8");
|
||||
return result;
|
||||
}
|
||||
Blend2D_Cache& detail::Frame_2D_Access::render_target(Frame_2D* frame) {
|
||||
if (!frame) throw std::invalid_argument("Render_Scene_2D requires a non-null external frame");
|
||||
if (!frame)
|
||||
throw std::invalid_argument("Render_Scene_2D requires a non-null external frame");
|
||||
return frame->d->color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "../render/Blend2D_Cache.hpp"
|
||||
#include <frame.hpp>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
namespace aethera::render_2d {
|
||||
class Frame_2D;
|
||||
@@ -11,12 +12,20 @@ struct Frame_2D_Access {
|
||||
}
|
||||
class Frame_2D final : public Render_Frame {
|
||||
public:
|
||||
explicit Frame_2D(Frame_Identity identity);
|
||||
static constexpr Pixel_Format native_pixel_format{
|
||||
Pixel_Format::bgra8_premultiplied};
|
||||
static constexpr std::array supported_pixel_formats{
|
||||
native_pixel_format, Pixel_Format::rgba8};
|
||||
explicit Frame_2D(Frame_Identity identity,
|
||||
Pixel_Format output_format = native_pixel_format);
|
||||
~Frame_2D() override;
|
||||
[[nodiscard]] Pixel_Format output_format() const noexcept;
|
||||
[[nodiscard]] Image_View image() const;
|
||||
/* 按本帧声明的输出格式生成连续像素;原生格式只做必要的行打包。 */
|
||||
[[nodiscard]] Pixel_Buffer output_pixels() const;
|
||||
private:
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d; /* 本帧最终二维颜色结果的唯一所有权。 */
|
||||
std::unique_ptr<Private> d; /* 最终二维颜色层及本帧输出格式。 */
|
||||
friend struct detail::Frame_2D_Access;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,14 +141,23 @@ enum class Line_Interpolation_Mode : std::uint8_t {
|
||||
step_right,
|
||||
cubic_value
|
||||
};
|
||||
enum class Pixel_Format : std::uint8_t { premultiplied_32 };
|
||||
enum class Pixel_Format : std::uint8_t {
|
||||
bgra8_premultiplied,
|
||||
rgba8
|
||||
};
|
||||
struct Pixel_Buffer {
|
||||
std::vector<std::byte> bytes{};
|
||||
int width{};
|
||||
int height{};
|
||||
Pixel_Format format{Pixel_Format::bgra8_premultiplied};
|
||||
};
|
||||
/* 只读图像像素视图。 */
|
||||
struct Image_View {
|
||||
const std::byte* data{}; /* 首行像素地址;不拥有内存。 */
|
||||
int width{}; /* 图像宽度,单位为像素。 */
|
||||
int height{}; /* 图像高度,单位为像素。 */
|
||||
int stride{}; /* 相邻两行起点的字节距离。 */
|
||||
Pixel_Format format{Pixel_Format::premultiplied_32}; /* 像素存储格式。 */
|
||||
Pixel_Format format{Pixel_Format::bgra8_premultiplied}; /* 像素存储格式。 */
|
||||
[[nodiscard]] bool empty() const noexcept {
|
||||
return data == nullptr || width <= 0 || height <= 0;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ Image_View Blend2D_Cache::view() const {
|
||||
BLImageData data{};
|
||||
Private::require_success(d->image.get_data(&data), "read Blend2D cache");
|
||||
return {static_cast<const std::byte*>(data.pixel_data), data.size.w, data.size.h,
|
||||
static_cast<int>(data.stride), Pixel_Format::premultiplied_32};
|
||||
static_cast<int>(data.stride), Pixel_Format::bgra8_premultiplied};
|
||||
}
|
||||
namespace detail {
|
||||
void Painter::Private::load_font_face(BLFontFace& face, std::initializer_list<const char*> candidates) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#include <render_2D/base/Frame_2D.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
namespace {
|
||||
using namespace aethera;
|
||||
using namespace aethera::render_2d;
|
||||
void paint_translucent_red(Frame_2D& frame) {
|
||||
auto& target = detail::Frame_2D_Access::render_target(&frame);
|
||||
detail::Painter painter(target, Size{1, 1});
|
||||
painter.rect(Rect_F{0.0, 0.0, 1.0, 1.0},
|
||||
Pen{Color::transparent(), 0.0, Line_Style::none},
|
||||
Brush{Color{255, 0, 0, 128}, Brush_Style::solid});
|
||||
}
|
||||
}
|
||||
TEST(frame_pixel_format, publishes_native_bgra_premultiplied_by_default) {
|
||||
Frame_2D frame{Frame_Identity{1, 0}};
|
||||
paint_translucent_red(frame);
|
||||
const auto pixels = frame.output_pixels();
|
||||
ASSERT_EQ(pixels.format, Pixel_Format::bgra8_premultiplied);
|
||||
ASSERT_EQ(pixels.bytes.size(), 4U);
|
||||
EXPECT_EQ(std::to_integer<unsigned>(pixels.bytes[0]), 0U);
|
||||
EXPECT_EQ(std::to_integer<unsigned>(pixels.bytes[1]), 0U);
|
||||
EXPECT_EQ(std::to_integer<unsigned>(pixels.bytes[2]), 128U);
|
||||
EXPECT_EQ(std::to_integer<unsigned>(pixels.bytes[3]), 128U);
|
||||
}
|
||||
TEST(frame_pixel_format, uses_blend2d_to_publish_straight_rgba) {
|
||||
Frame_2D frame{Frame_Identity{2, 0}, Pixel_Format::rgba8};
|
||||
paint_translucent_red(frame);
|
||||
const auto pixels = frame.output_pixels();
|
||||
ASSERT_EQ(pixels.format, Pixel_Format::rgba8);
|
||||
ASSERT_EQ(pixels.bytes.size(), 4U);
|
||||
EXPECT_GE(std::to_integer<unsigned>(pixels.bytes[0]), 254U);
|
||||
EXPECT_EQ(std::to_integer<unsigned>(pixels.bytes[1]), 0U);
|
||||
EXPECT_EQ(std::to_integer<unsigned>(pixels.bytes[2]), 0U);
|
||||
EXPECT_EQ(std::to_integer<unsigned>(pixels.bytes[3]), 128U);
|
||||
}
|
||||
@@ -2,13 +2,16 @@
|
||||
#include <stdexcept>
|
||||
namespace aethera::render_3d {
|
||||
struct Frame_3D::Private {
|
||||
Pixel_Format output_format{Frame_3D::native_pixel_format};
|
||||
Extent extent{}; /* 本帧 GPU 读回图像尺寸。 */
|
||||
std::shared_ptr<const std::vector<std::byte>> pixels{}; /* 合并提交共享的不可变 RGBA8 像素。 */
|
||||
Frame_3D_Output output{Frame_3D_Output::pixels}; /* 调用方要求的最终帧输出。 */
|
||||
};
|
||||
Frame_3D::Frame_3D(Frame_Identity identity, Frame_3D_Output output)
|
||||
Frame_3D::Frame_3D(Frame_Identity identity, Frame_3D_Output output,
|
||||
Pixel_Format output_format)
|
||||
: Render_Frame(identity), d(std::make_unique<Private>()) {
|
||||
d->output = output;
|
||||
d->output_format = output_format;
|
||||
}
|
||||
Frame_3D::~Frame_3D() = default;
|
||||
Extent Frame_3D::extent() const noexcept { return d->extent; }
|
||||
@@ -17,6 +20,7 @@ std::span<const std::byte> Frame_3D::pixels() const noexcept {
|
||||
return *d->pixels;
|
||||
}
|
||||
Frame_3D_Output Frame_3D::output() const noexcept { return d->output; }
|
||||
Pixel_Format Frame_3D::output_format() const noexcept { return d->output_format; }
|
||||
void detail::Frame_3D_Access::assign_pixels(
|
||||
std::span<Frame_3D* const> frames, Extent extent,
|
||||
std::vector<std::byte> pixels) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "Types.hpp"
|
||||
#include <frame.hpp>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
@@ -19,11 +20,16 @@ struct Frame_3D_Access {
|
||||
}
|
||||
class Frame_3D final : public Render_Frame {
|
||||
public:
|
||||
explicit Frame_3D(Frame_Identity identity, Frame_3D_Output output = Frame_3D_Output::pixels);
|
||||
static constexpr Pixel_Format native_pixel_format{Pixel_Format::rgba8_unorm};
|
||||
static constexpr std::array supported_pixel_formats{native_pixel_format};
|
||||
explicit Frame_3D(Frame_Identity identity,
|
||||
Frame_3D_Output output = Frame_3D_Output::pixels,
|
||||
Pixel_Format output_format = native_pixel_format);
|
||||
~Frame_3D() override;
|
||||
[[nodiscard]] Extent extent() const noexcept;
|
||||
[[nodiscard]] std::span<const std::byte> pixels() const noexcept;
|
||||
[[nodiscard]] Frame_3D_Output output() const noexcept;
|
||||
[[nodiscard]] Pixel_Format output_format() const noexcept;
|
||||
private:
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d; /* 本帧最终三维读回结果的唯一所有权。 */
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
namespace aethera::render_3d {
|
||||
using Coordinate_3D = float;
|
||||
using Pixel_Distance = float;
|
||||
/* Datoviz 离屏目标与读回缓冲之间的实际字节布局。 */
|
||||
enum class Pixel_Format : std::uint8_t {
|
||||
rgba8_unorm
|
||||
};
|
||||
struct Vec2 {
|
||||
Coordinate_3D x{}; /* 水平分量。 */
|
||||
Coordinate_3D y{}; /* 垂直分量。 */
|
||||
|
||||
@@ -18,7 +18,7 @@ struct Graph_WebSocket::Private {
|
||||
};
|
||||
Graph_WebSocket::Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr<Plot> plot) : d(std::make_unique<Private>()) { d->connection = connection; d->plot = std::move(plot); d->owner = connection.get(); }
|
||||
Graph_WebSocket::~Graph_WebSocket() { close(); }
|
||||
void Graph_WebSocket::start() { if (d->attached) return; const auto weak = weak_from_this(); d->plot->attach(d->owner, [weak](std::shared_ptr<const Plot_Frame_Message> frame) { const auto socket = weak.lock(); if (!socket) return; const auto connection = socket->d->connection.lock(); if (!connection || !connection->connected()) return; connection->send(frame->metadata, drogon::WebSocketMessageType::Text); if (!frame->pixels.empty()) connection->send(frame->pixels.data(), frame->pixels.size(), drogon::WebSocketMessageType::Binary); }); d->attached = true; }
|
||||
void Graph_WebSocket::start() { if (d->attached) return; const auto weak = weak_from_this(); d->plot->attach(d->owner, [weak](std::shared_ptr<const Plot_Frame_Message> frame) { const auto socket = weak.lock(); if (!socket) return; const auto connection = socket->d->connection.lock(); if (!connection || !connection->connected()) return; connection->send(frame->metadata, drogon::WebSocketMessageType::Text); if (!frame->pixels.empty()) connection->send(reinterpret_cast<const char*>(frame->pixels.data()), frame->pixels.size(), drogon::WebSocketMessageType::Binary); }); d->attached = true; }
|
||||
void Graph_WebSocket::receive(std::string_view message) {
|
||||
const auto json = nlohmann::json::parse(message, nullptr, false);
|
||||
if (json.is_discarded() || !json.is_object()) return;
|
||||
@@ -33,6 +33,16 @@ void Graph_WebSocket::receive(std::string_view message) {
|
||||
request.time_milliseconds = value->get<double>();
|
||||
if (const auto value = json.find("delivery"); value != json.end() && value->is_string())
|
||||
request.delivery = *value == "diagnostics" ? Plot_Frame_Delivery::diagnostics : Plot_Frame_Delivery::pixels;
|
||||
if (const auto value = json.find("pixel_format");
|
||||
value != json.end() && value->is_string()) {
|
||||
if (*value == "bgra8-premultiplied")
|
||||
request.pixel_format = render_2d::Pixel_Format::bgra8_premultiplied;
|
||||
else if (*value == "rgba8")
|
||||
request.pixel_format = render_2d::Pixel_Format::rgba8;
|
||||
else if (*value == "rgba8-unorm")
|
||||
request.pixel_format = render_3d::Pixel_Format::rgba8_unorm;
|
||||
else return;
|
||||
}
|
||||
if (const auto viewport = json.find("viewport"); viewport != json.end() && viewport->is_object()) {
|
||||
if (const auto value = viewport->find("width"); value != viewport->end() && value->is_number_unsigned())
|
||||
request.width = std::clamp(value->get<std::uint32_t>(), 160U, 1920U);
|
||||
|
||||
+66
-24
@@ -33,7 +33,7 @@ using Frequency_Axis_Object = Impl<Frequency_Axis>;
|
||||
using Numeric_Axis_Object = Impl<Numeric_Axis>;
|
||||
using Time_Axis_Object = Impl<Time_Axis>;
|
||||
using Selection_Object = Impl<Selection_Rectangle_Overlay>;
|
||||
constexpr std::uint16_t frame_protocol_version{5};
|
||||
constexpr std::uint16_t frame_protocol_version{6};
|
||||
enum class Frame_Pacing_Mode : std::uint32_t {
|
||||
manual,
|
||||
fixed_rate,
|
||||
@@ -77,6 +77,31 @@ std::string_view delivery_name(Plot_Frame_Delivery delivery) {
|
||||
}
|
||||
throw std::logic_error("unknown plot frame delivery");
|
||||
}
|
||||
std::string_view pixel_format_name(render_2d::Pixel_Format format) {
|
||||
switch (format) {
|
||||
case render_2d::Pixel_Format::bgra8_premultiplied: return "bgra8-premultiplied";
|
||||
case render_2d::Pixel_Format::rgba8: return "rgba8";
|
||||
}
|
||||
throw std::logic_error("unknown 2D pixel format");
|
||||
}
|
||||
std::string_view pixel_format_name(render_3d::Pixel_Format format) {
|
||||
switch (format) {
|
||||
case render_3d::Pixel_Format::rgba8_unorm: return "rgba8-unorm";
|
||||
}
|
||||
throw std::logic_error("unknown 3D pixel format");
|
||||
}
|
||||
nlohmann::json supported_2d_pixel_formats() {
|
||||
auto result = nlohmann::json::array();
|
||||
for (const auto format : Frame_2D::supported_pixel_formats)
|
||||
result.push_back(pixel_format_name(format));
|
||||
return result;
|
||||
}
|
||||
nlohmann::json supported_3d_pixel_formats() {
|
||||
auto result = nlohmann::json::array();
|
||||
for (const auto format : Frame_3D::supported_pixel_formats)
|
||||
result.push_back(pixel_format_name(format));
|
||||
return result;
|
||||
}
|
||||
Frame_Pacing_Properties Frame_Policy::snapshot() const {
|
||||
std::lock_guard lock(mutex);
|
||||
return pacing;
|
||||
@@ -114,7 +139,13 @@ nlohmann::json Frame_Policy::write_prop(std::string_view key, const nlohmann::js
|
||||
}
|
||||
return {{"success", false}, {"error", "unknown frame runtime property"}};
|
||||
}
|
||||
nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width, std::uint32_t height, std::size_t byte_length, Plot_Frame_Delivery delivery, const Frame_Pacing_Properties& pacing) {
|
||||
nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width,
|
||||
std::uint32_t height, std::size_t byte_length,
|
||||
std::string_view pixel_format,
|
||||
std::string_view native_pixel_format,
|
||||
nlohmann::json supported_pixel_formats,
|
||||
Plot_Frame_Delivery delivery,
|
||||
const Frame_Pacing_Properties& pacing) {
|
||||
nlohmann::json markers = nlohmann::json::object();
|
||||
for (const auto& point : frame.trace_points()) markers[std::string(magic_enum::enum_name(point.marker))] = point.elapsed_ns;
|
||||
nlohmann::json measurements = nlohmann::json::object();
|
||||
@@ -124,7 +155,11 @@ nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width, std::uin
|
||||
{"kind", "frame_metadata"}, {"protocol", "aethera.frame"}, {"version", frame_protocol_version},
|
||||
{"sequence", identity.sequence}, {"correlation_id", identity.correlation_id},
|
||||
{"created_time_unix_ms", static_cast<double>(frame.created_time_unix_ns()) / 1'000'000.0}, {"delivery", delivery_name(delivery)},
|
||||
{"pixel", {{"width", width}, {"height", height}, {"format", "rgba8"}, {"byte_length", byte_length}}},
|
||||
{"pixel", {{"width", width}, {"height", height},
|
||||
{"format", pixel_format},
|
||||
{"native_format", native_pixel_format},
|
||||
{"supported_formats", std::move(supported_pixel_formats)},
|
||||
{"byte_length", byte_length}}},
|
||||
{"pacing", {{"mode", pacing_mode_name(pacing.mode)}, {"fixed_rate_fps", pacing.fixed_rate_fps}, {"minimum_latency_headroom", pacing.minimum_latency_headroom}}},
|
||||
{"trace", {{"clock", "steady_elapsed_ns"}, {"markers", std::move(markers)}, {"measurements", std::move(measurements)}}}
|
||||
};
|
||||
@@ -132,35 +167,32 @@ nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width, std::uin
|
||||
std::shared_ptr<const Plot_Frame_Message> encode_frame(Frame_2D* frame, Plot_Frame_Delivery delivery, const Frame_Pacing_Properties& pacing) {
|
||||
frame->mark(Frame_Trace_Marker::websocket_publish_started);
|
||||
const Image_View image = frame->image();
|
||||
std::string output;
|
||||
if (delivery == Plot_Frame_Delivery::pixels) {
|
||||
output.reserve(static_cast<std::size_t>(image.width) * image.height * 4);
|
||||
for (int y = 0; y < image.height; ++y) {
|
||||
const auto* row = reinterpret_cast<const std::uint8_t*>(
|
||||
image.data + static_cast<std::ptrdiff_t>(y) * image.stride);
|
||||
for (int x = 0; x < image.width; ++x) {
|
||||
const auto* pixel = row + x * 4;
|
||||
output.push_back(static_cast<char>(pixel[2]));
|
||||
output.push_back(static_cast<char>(pixel[1]));
|
||||
output.push_back(static_cast<char>(pixel[0]));
|
||||
output.push_back(static_cast<char>(pixel[3]));
|
||||
}
|
||||
}
|
||||
}
|
||||
Pixel_Buffer output{{}, image.width, image.height, frame->output_format()};
|
||||
if (delivery == Plot_Frame_Delivery::pixels) output = frame->output_pixels();
|
||||
frame->mark(Frame_Trace_Marker::websocket_publish_finished);
|
||||
auto message = std::make_shared<Plot_Frame_Message>();
|
||||
message->pixels = std::move(output);
|
||||
message->metadata = frame_metadata(*frame, static_cast<std::uint32_t>(image.width), static_cast<std::uint32_t>(image.height), message->pixels.size(), delivery, pacing).dump();
|
||||
message->pixels = std::move(output.bytes);
|
||||
message->metadata = frame_metadata(
|
||||
*frame, static_cast<std::uint32_t>(output.width),
|
||||
static_cast<std::uint32_t>(output.height), message->pixels.size(),
|
||||
pixel_format_name(output.format),
|
||||
pixel_format_name(Frame_2D::native_pixel_format),
|
||||
supported_2d_pixel_formats(), delivery, pacing).dump();
|
||||
return message;
|
||||
}
|
||||
std::shared_ptr<const Plot_Frame_Message> encode_frame(Frame_3D* frame, Plot_Frame_Delivery delivery, const Frame_Pacing_Properties& pacing) {
|
||||
frame->mark(Frame_Trace_Marker::websocket_publish_started);
|
||||
const auto pixels = frame->pixels();
|
||||
auto message = std::make_shared<Plot_Frame_Message>();
|
||||
if (delivery == Plot_Frame_Delivery::pixels) message->pixels.assign(reinterpret_cast<const char*>(pixels.data()), pixels.size());
|
||||
if (delivery == Plot_Frame_Delivery::pixels)
|
||||
message->pixels.assign(pixels.begin(), pixels.end());
|
||||
frame->mark(Frame_Trace_Marker::websocket_publish_finished);
|
||||
const auto extent = frame->extent();
|
||||
message->metadata = frame_metadata(*frame, extent.width, extent.height, message->pixels.size(), delivery, pacing).dump();
|
||||
message->metadata = frame_metadata(
|
||||
*frame, extent.width, extent.height, message->pixels.size(),
|
||||
pixel_format_name(frame->output_format()),
|
||||
pixel_format_name(Frame_3D::native_pixel_format),
|
||||
supported_3d_pixel_formats(), delivery, pacing).dump();
|
||||
return message;
|
||||
}
|
||||
struct Schema_Query {
|
||||
@@ -281,9 +313,19 @@ nlohmann::json Plot::Private::schema() const {
|
||||
}
|
||||
Plot::Private::Managed_Frame Plot::Private::make_frame(Frame_Submission submission) {
|
||||
const Frame_Identity identity{next_frame_sequence++, submission.request.correlation_id};
|
||||
if (std::holds_alternative<std::unique_ptr<Scene_2D>>(scene)) return {submission.owner, std::move(submission.request), std::make_unique<Frame_2D>(identity)};
|
||||
if (std::holds_alternative<std::unique_ptr<Scene_2D>>(scene)) {
|
||||
const auto* requested = std::get_if<render_2d::Pixel_Format>(
|
||||
&submission.request.pixel_format);
|
||||
const auto format = requested ? *requested : Frame_2D::native_pixel_format;
|
||||
return {submission.owner, std::move(submission.request),
|
||||
std::make_unique<Frame_2D>(identity, format)};
|
||||
}
|
||||
const auto output = submission.request.delivery == Plot_Frame_Delivery::pixels ? Frame_3D_Output::pixels : Frame_3D_Output::diagnostics;
|
||||
return {submission.owner, std::move(submission.request), std::make_unique<Frame_3D>(identity, output)};
|
||||
const auto* requested = std::get_if<render_3d::Pixel_Format>(
|
||||
&submission.request.pixel_format);
|
||||
const auto format = requested ? *requested : Frame_3D::native_pixel_format;
|
||||
return {submission.owner, std::move(submission.request),
|
||||
std::make_unique<Frame_3D>(identity, output, format)};
|
||||
}
|
||||
void Plot::Private::request_frame(Frame_Submission submission) {
|
||||
auto frame = make_frame(std::move(submission));
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
namespace aethera::web {
|
||||
struct Plot_Input_Event {
|
||||
Event_Type type{Event_Type::pointer_move};
|
||||
@@ -34,10 +36,12 @@ struct Plot_Frame_Request {
|
||||
std::uint32_t width{720}; /* 请求帧宽度,单位为物理像素。 */
|
||||
std::uint32_t height{420}; /* 请求帧高度,单位为物理像素。 */
|
||||
Plot_Frame_Delivery delivery{Plot_Frame_Delivery::pixels}; /* 本次请求返回完整像素或仅返回诊断元数据。 */
|
||||
std::variant<render_2d::Pixel_Format, render_3d::Pixel_Format> pixel_format{
|
||||
render_2d::Frame_2D::native_pixel_format}; /* 二维帧发布格式;三维固定为原生 RGBA8。 */
|
||||
};
|
||||
struct Plot_Frame_Message {
|
||||
std::string metadata{}; /* 单帧 JSON 元数据 WebSocket 文本消息。 */
|
||||
std::string pixels{}; /* 紧随元数据发送的纯 RGBA8 WebSocket 二进制消息。 */
|
||||
std::vector<std::byte> pixels{}; /* 格式由元数据声明的像素所有权。 */
|
||||
};
|
||||
class Plot final : public std::enable_shared_from_this<Plot> {
|
||||
public:
|
||||
|
||||
+127
-15
@@ -28,9 +28,10 @@ type State_Histories = Record<string, number[]>;
|
||||
type Stream_Status = "IDLE" | "CONNECTING" | "LIVE" | "OFFLINE";
|
||||
type Frame_Pacing_Mode = "manual" | "fixed_rate" | "minimum_latency" | "maximum_rate";
|
||||
type Frame_Delivery = "pixels" | "diagnostics";
|
||||
type Frame_Metadata = {kind: "frame_metadata"; protocol: "aethera.frame"; version: 5; sequence: number; correlation_id: number;
|
||||
type Pixel_Format = "bgra8-premultiplied" | "rgba8" | "rgba8-unorm";
|
||||
type Frame_Metadata = {kind: "frame_metadata"; protocol: "aethera.frame"; version: 6; sequence: number; correlation_id: number;
|
||||
delivery: Frame_Delivery;
|
||||
created_time_unix_ms: number; pixel: {width: number; height: number; format: "rgba8"; byte_length: number};
|
||||
created_time_unix_ms: number; pixel: {width: number; height: number; format: Pixel_Format; native_format: Pixel_Format; supported_formats: Pixel_Format[]; byte_length: number};
|
||||
pacing: {mode: Frame_Pacing_Mode; fixed_rate_fps: number; minimum_latency_headroom: number};
|
||||
trace: {clock: "steady_elapsed_ns"; markers: Record<string, number>; measurements: Record<string, number>}};
|
||||
type Frame_Stage_Values = Record<string, number>;
|
||||
@@ -57,22 +58,124 @@ function local_time_milliseconds() {
|
||||
function valid_frame_metadata(value: unknown): value is Frame_Metadata {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const frame = value as Partial<Frame_Metadata>;
|
||||
return frame.kind === "frame_metadata" && frame.protocol === "aethera.frame" && frame.version === 5
|
||||
return frame.kind === "frame_metadata" && frame.protocol === "aethera.frame" && frame.version === 6
|
||||
&& typeof frame.sequence === "number" && typeof frame.correlation_id === "number"
|
||||
&& (frame.delivery === "pixels" || frame.delivery === "diagnostics")
|
||||
&& Boolean(frame.pixel) && frame.pixel?.format === "rgba8" && Boolean(frame.trace);
|
||||
&& Boolean(frame.pixel) && (frame.pixel?.format === "rgba8" || frame.pixel?.format === "bgra8-premultiplied" || frame.pixel?.format === "rgba8-unorm")
|
||||
&& (frame.pixel?.native_format === "rgba8" || frame.pixel?.native_format === "bgra8-premultiplied" || frame.pixel?.native_format === "rgba8-unorm")
|
||||
&& Array.isArray(frame.pixel?.supported_formats)
|
||||
&& frame.pixel!.supported_formats.includes(frame.pixel!.format)
|
||||
&& Boolean(frame.trace);
|
||||
}
|
||||
|
||||
function draw_pixels(canvas: HTMLCanvasElement, bytes: ArrayBuffer, metadata: Frame_Metadata) {
|
||||
const {width, height, byte_length} = metadata.pixel;
|
||||
type Pixel_Renderer = {
|
||||
preferred_2d_format: Pixel_Format;
|
||||
draw: (bytes: ArrayBuffer, metadata: Frame_Metadata) => number | null;
|
||||
};
|
||||
|
||||
function shader(gl: WebGL2RenderingContext, type: number, source: string) {
|
||||
const value = gl.createShader(type);
|
||||
if (!value) return null;
|
||||
gl.shaderSource(value, source);
|
||||
gl.compileShader(value);
|
||||
if (gl.getShaderParameter(value, gl.COMPILE_STATUS)) return value;
|
||||
gl.deleteShader(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
function webgl_pixel_renderer(canvas: HTMLCanvasElement, gl: WebGL2RenderingContext): Pixel_Renderer | null {
|
||||
const vertex = shader(gl, gl.VERTEX_SHADER, `#version 300 es
|
||||
precision highp float;
|
||||
const vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
out vec2 texture_coordinate;
|
||||
void main() {
|
||||
vec2 position = positions[gl_VertexID];
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
texture_coordinate = vec2(position.x * 0.5 + 0.5, 1.0 - (position.y * 0.5 + 0.5));
|
||||
}`);
|
||||
const fragment = shader(gl, gl.FRAGMENT_SHADER, `#version 300 es
|
||||
precision highp float;
|
||||
uniform sampler2D frame_texture;
|
||||
uniform bool source_is_bgra;
|
||||
in vec2 texture_coordinate;
|
||||
out vec4 output_color;
|
||||
void main() {
|
||||
vec4 color = texture(frame_texture, texture_coordinate);
|
||||
output_color = source_is_bgra ? color.bgra : color;
|
||||
}`);
|
||||
if (!vertex || !fragment) return null;
|
||||
const program = gl.createProgram();
|
||||
if (!program) return null;
|
||||
gl.attachShader(program, vertex);
|
||||
gl.attachShader(program, fragment);
|
||||
gl.linkProgram(program);
|
||||
gl.deleteShader(vertex);
|
||||
gl.deleteShader(fragment);
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
gl.deleteProgram(program);
|
||||
return null;
|
||||
}
|
||||
const texture = gl.createTexture();
|
||||
const vertex_array = gl.createVertexArray();
|
||||
if (!texture || !vertex_array) return null;
|
||||
const format_location = gl.getUniformLocation(program, "source_is_bgra");
|
||||
gl.useProgram(program);
|
||||
gl.uniform1i(gl.getUniformLocation(program, "frame_texture"), 0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
||||
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
|
||||
gl.disable(gl.BLEND);
|
||||
return {
|
||||
preferred_2d_format: "bgra8-premultiplied",
|
||||
draw: (bytes, metadata) => {
|
||||
const {width, height, byte_length, format} = metadata.pixel;
|
||||
if (bytes.byteLength !== byte_length || byte_length !== width * height * 4) return null;
|
||||
const started = performance.now();
|
||||
const pixels = new Uint8ClampedArray(bytes);
|
||||
if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; }
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
}
|
||||
gl.viewport(0, 0, width, height);
|
||||
gl.useProgram(program);
|
||||
gl.bindVertexArray(vertex_array);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA,
|
||||
gl.UNSIGNED_BYTE, new Uint8Array(bytes));
|
||||
gl.uniform1i(format_location, format === "bgra8-premultiplied" ? 1 : 0);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
return performance.now() - started;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function pixel_renderer(canvas: HTMLCanvasElement): Pixel_Renderer | null {
|
||||
const gl = canvas.getContext("webgl2", {
|
||||
alpha: false, antialias: false, depth: false, stencil: false,
|
||||
premultipliedAlpha: true, preserveDrawingBuffer: false
|
||||
});
|
||||
if (gl) return webgl_pixel_renderer(canvas, gl);
|
||||
const context = canvas.getContext("2d", {alpha: false});
|
||||
if (!context) return null;
|
||||
context.putImageData(new ImageData(pixels, width, height), 0, 0);
|
||||
return {
|
||||
preferred_2d_format: "rgba8",
|
||||
draw: (bytes, metadata) => {
|
||||
const {width, height, byte_length, format} = metadata.pixel;
|
||||
if ((format !== "rgba8" && format !== "rgba8-unorm") || bytes.byteLength !== byte_length ||
|
||||
byte_length !== width * height * 4) return null;
|
||||
const started = performance.now();
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
}
|
||||
context.putImageData(new ImageData(new Uint8ClampedArray(bytes), width, height), 0, 0);
|
||||
return performance.now() - started;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function percentile(values: number[], ratio: number) {
|
||||
@@ -271,6 +374,15 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
|
||||
set_status("IDLE");
|
||||
return;
|
||||
}
|
||||
const canvas = canvas_ref.current;
|
||||
const renderer = canvas ? pixel_renderer(canvas) : null;
|
||||
if (!renderer) {
|
||||
socket_ref.current = null;
|
||||
set_status("OFFLINE");
|
||||
return;
|
||||
}
|
||||
const requested_pixel_format: Pixel_Format = plot.dimension === "2D"
|
||||
? renderer.preferred_2d_format : "rgba8-unorm";
|
||||
let stopped = false;
|
||||
let timer = 0;
|
||||
let diagnostics_timer = 0;
|
||||
@@ -331,7 +443,8 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
|
||||
const request_id = next_request_id++;
|
||||
const started_at = performance.now();
|
||||
const delivery: Frame_Delivery = stream_pixels_ref.current ? "pixels" : "diagnostics";
|
||||
socket.send(JSON.stringify({...message, request_id, delivery}));
|
||||
socket.send(JSON.stringify({...message, request_id, delivery,
|
||||
pixel_format: requested_pixel_format}));
|
||||
request_started_at.set(request_id, started_at);
|
||||
frame_pending = true;
|
||||
manual_frame_pending = false;
|
||||
@@ -418,10 +531,9 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
|
||||
const receive_pixels = (bytes: ArrayBuffer) => {
|
||||
const pair = pending_metadata;
|
||||
pending_metadata = null;
|
||||
const canvas = canvas_ref.current;
|
||||
if (!pair || !canvas) return;
|
||||
if (!pair) return;
|
||||
const completed_at = performance.now();
|
||||
const canvas_upload_ms = draw_pixels(canvas, bytes, pair.value);
|
||||
const canvas_upload_ms = renderer.draw(bytes, pair.value);
|
||||
if (canvas_upload_ms !== null) complete_frame(pair, completed_at, canvas_upload_ms, true);
|
||||
else {
|
||||
frame_pending = false;
|
||||
@@ -777,7 +889,7 @@ const pipeline_2d_definitions: Pipeline_Stage_Definition[] = [
|
||||
["pipeline_2d_paint_ms", "Blend2D 绘制", "二维 Paint 任务图清屏并写入 Blend2D 帧缓存的耗时。"],
|
||||
["pipeline_2d_scene_coordination_ms", "2D Scene 编排", "Scene 渲染区间内除事件、Prepare、Paint 外的依赖图编排耗时。"],
|
||||
["pipeline_2d_callback_ms", "2D 完成回调", "同步二维帧完成后回调到 Plot 发布线程的耗时。"],
|
||||
["pipeline_2d_encode_ms", "BGRA→RGBA 编码", "逐行把 Blend2D BGRA 帧缓存转换成 WebSocket RGBA 载荷的耗时。"],
|
||||
["pipeline_2d_encode_ms", "2D 像素封装", "原生 BGRA 帧只进行连续行打包;仅在浏览器不支持 WebGL2 时由 Blend2D 转换为 RGBA。"],
|
||||
["pipeline_2d_frame_handoff_ms", "2D 帧建立与发布调度", "Render_Frame 建立、进入 Scene 以及编码完成后生成元数据的调度间隙。"]
|
||||
];
|
||||
const pipeline_3d_definitions: Pipeline_Stage_Definition[] = [
|
||||
@@ -1069,7 +1181,7 @@ const Plot_Card = memo(function Plot_Card({plot, selected, policy, on_policy, on
|
||||
<div className="plotExecutionPolicy" onPointerDown={event => event.stopPropagation()}>
|
||||
<label title="控制这张图的画面是否显示;不改变 Scene 自身的 visible 属性。"><input type="checkbox" checked={policy.visible} onChange={event => on_policy(plot.id, {visible: event.target.checked})}/>显示画面</label>
|
||||
<label title="关闭显示画面后,是否仍持续请求真实后端帧并采集诊断。"><input type="checkbox" checked={policy.refresh_hidden} onChange={event => on_policy(plot.id, {refresh_hidden: event.target.checked})}/>隐藏时继续采样</label>
|
||||
<label title="开启时传输完整 RGBA 像素;关闭时只传诊断 JSON,用于隔离 GPU 渲染与 WebSocket 像素传输开销。"><input type="checkbox" checked={policy.transfer_pixels} onChange={event => on_policy(plot.id, {transfer_pixels: event.target.checked})}/>WebSocket 像素</label>
|
||||
<label title="开启时传输后端声明格式的完整像素;关闭时只传诊断 JSON,用于隔离渲染与 WebSocket 像素传输开销。"><input type="checkbox" checked={policy.transfer_pixels} onChange={event => on_policy(plot.id, {transfer_pixels: event.target.checked})}/>WebSocket 像素</label>
|
||||
</div>
|
||||
{plot.description ? <p>{plot.description}</p> : null}<div className={`plotViewport${policy.visible ? "" : " plotViewportHidden"}`}><canvas ref={canvas_ref} tabIndex={0}/>{!policy.visible ? <div className="plotHiddenState"><strong>画面不可见</strong><span>{policy.refresh_hidden ? "后端仍在渲染与采样" : "后端帧请求已停止"}</span></div> : null}</div></article>;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user