Draw the client's scene in the renderer process
The adapter records everything the client draws (state, transforms, lights, fog, textures and buffers) as a command stream, sent in chunks through the shared memory channel, while IMVU's Direct3D renderer still draws the whole view. The renderer process draws it with Diligent, with shaders reproducing IMVU's Direct3D 9 fixed-function setup, over the right half of the 3D view to compare. It skips to the newest frame when it falls behind, and logs its errors and frame times. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
5c35c8b63a
commit
42fc55cb91
23 files changed
+2121
-472
No files matched your search
@@ -7,19 +7,36 @@
|
||||
|
||||
namespace t5de::protocol {
|
||||
|
||||
inline constexpr std::uint32_t version = 1;
|
||||
inline constexpr std::uint32_t version = 2;
|
||||
|
||||
// A shared memory section the adapter creates for each engine. Its name is passed to the renderer process as
|
||||
// `--channel <name>`; the event "<name>-frame" (auto-reset) is set after each frame the client draws.
|
||||
// Which part of the client's 3D view the renderer process draws (the client's own renderer draws all of it, under).
|
||||
enum class View : std::uint32_t {
|
||||
Off, // none
|
||||
Split, // the right half, to compare with the client's renderer on the left
|
||||
Full,
|
||||
};
|
||||
|
||||
// A shared memory section the adapter creates for each engine, named by the renderer process's `--channel <name>`
|
||||
// argument: this header, then `capacity` bytes of data. The adapter sends the command stream (commands.hpp) through
|
||||
// it in chunks: it waits for the event "<name>-consumed", writes a chunk and sets "<name>-ready"; the renderer
|
||||
// process copies the chunk out and sets "<name>-consumed" (both auto-reset; consumed starts set).
|
||||
struct Channel {
|
||||
std::uint32_t version;
|
||||
std::uint32_t closing; // nonzero: the engine is gone, the renderer process should exit
|
||||
std::uint64_t window; // IMVU's 3D window (an HWND; window handles are valid in every process)
|
||||
std::uint64_t frames; // frames the client has drawn
|
||||
View view;
|
||||
std::uint32_t capacity;
|
||||
std::uint32_t chunkSize;
|
||||
std::uint32_t chunkFlags; // chunkEndsFrame
|
||||
};
|
||||
|
||||
static_assert(sizeof(Channel) == 24, "same layout in 32-bit and 64-bit builds");
|
||||
static_assert(sizeof(Channel) == 40, "same layout in 32-bit and 64-bit builds");
|
||||
|
||||
inline constexpr wchar_t frameEventSuffix[] = L"-frame";
|
||||
inline constexpr std::uint32_t chunkEndsFrame = 1;
|
||||
inline constexpr std::uint32_t channelCapacity = 16 * 1024 * 1024;
|
||||
|
||||
inline constexpr wchar_t readyEventSuffix[] = L"-ready";
|
||||
inline constexpr wchar_t consumedEventSuffix[] = L"-consumed";
|
||||
|
||||
} // namespace t5de::protocol
|
||||
@@ -0,0 +1,259 @@
|
||||
#pragma once
|
||||
// The command stream: what the adapter records from IMVU's renderer interface, and the renderer process carries out.
|
||||
// Each command is a Header, its fixed-size body, then for some a variable-size payload (pixels, vertex data),
|
||||
// padded to 4 bytes. Built for 32-bit and 64-bit: fixed-size fields only.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace t5de::protocol {
|
||||
|
||||
enum class Op : std::uint16_t {
|
||||
BeginFrame = 1,
|
||||
EndFrame,
|
||||
Clear,
|
||||
SetProjection,
|
||||
SetModel,
|
||||
SetView,
|
||||
SetTextures,
|
||||
SetState,
|
||||
SetLight,
|
||||
SetFog,
|
||||
SetTextureTransform,
|
||||
CreateTexture,
|
||||
UpdateTexture,
|
||||
DestroyTexture,
|
||||
CreateBuffer,
|
||||
UpdateBuffer,
|
||||
DestroyBuffer,
|
||||
DrawTriangles,
|
||||
DrawArrays,
|
||||
};
|
||||
|
||||
struct Header {
|
||||
Op op;
|
||||
std::uint16_t reserved;
|
||||
std::uint32_t size; // of the body and payload, padded to 4 bytes
|
||||
};
|
||||
|
||||
// Colors are 0xAARRGGBB. Resource ids are the adapter's, per engine; 0 means none.
|
||||
|
||||
struct BeginFrame {
|
||||
std::uint32_t offscreen; // drawn into a render target, not the window: nothing to show
|
||||
};
|
||||
|
||||
struct EndFrame {
|
||||
std::uint32_t unused;
|
||||
};
|
||||
|
||||
struct Clear {
|
||||
std::uint32_t flags; // 1: color, 2: depth (to 1)
|
||||
std::uint32_t color;
|
||||
};
|
||||
|
||||
// The viewport's size, and a Direct3D projection matrix (row-major, row vectors).
|
||||
struct SetProjection {
|
||||
std::int32_t width;
|
||||
std::int32_t height;
|
||||
float m[16];
|
||||
};
|
||||
|
||||
// An affine transform (SetModel, SetView): a 3x3 matrix, row-major, then the translation.
|
||||
struct SetTransform {
|
||||
float m[12];
|
||||
};
|
||||
|
||||
struct SetTextures {
|
||||
std::uint32_t texture0;
|
||||
std::uint32_t texture1;
|
||||
};
|
||||
|
||||
enum class State : std::uint32_t {
|
||||
DepthTest,
|
||||
DepthWrite,
|
||||
Blending,
|
||||
BlendMode, // 1: alpha, 2: additive, 3: premultiplied, 4: additive alpha
|
||||
AlphaTest,
|
||||
AlphaReference, // 0-255: pixels pass if their alpha is greater
|
||||
Culling,
|
||||
NormalInverting, // flips which winding faces front
|
||||
Lighting,
|
||||
Fog,
|
||||
Ambient, // a color
|
||||
};
|
||||
|
||||
struct SetState {
|
||||
State state;
|
||||
std::uint32_t value;
|
||||
};
|
||||
|
||||
struct SetLight {
|
||||
std::uint32_t index;
|
||||
std::uint32_t enabled;
|
||||
std::uint32_t color;
|
||||
std::uint32_t directional; // 0: a point light at vector; 1: light coming from direction vector
|
||||
float vector[3];
|
||||
};
|
||||
|
||||
struct SetFog {
|
||||
float start;
|
||||
float end;
|
||||
std::uint32_t color;
|
||||
};
|
||||
|
||||
// Texture coordinates for a stage: u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5].
|
||||
struct SetTextureTransform {
|
||||
std::uint32_t stage;
|
||||
std::uint32_t identity;
|
||||
float m[6];
|
||||
};
|
||||
|
||||
// Payload: width * height pixels (0xAARRGGBB), rows packed.
|
||||
struct CreateTexture {
|
||||
std::uint32_t id;
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
std::uint32_t flags; // 2: wrap u, 4: wrap v (else clamp)
|
||||
};
|
||||
|
||||
struct UpdateTexture {
|
||||
std::uint32_t id;
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
};
|
||||
|
||||
struct DestroyTexture {
|
||||
std::uint32_t id;
|
||||
};
|
||||
|
||||
enum class BufferKind : std::uint32_t {
|
||||
Index, // 16-bit indices
|
||||
Vertex, // positions and normals (3 floats), texture coordinates (2 floats) or colors (0xAARRGGBB)
|
||||
};
|
||||
|
||||
// Payload: count elements of elementSize bytes, packed; empty for a buffer filled later (UpdateBuffer).
|
||||
struct CreateBuffer {
|
||||
std::uint32_t id;
|
||||
BufferKind kind;
|
||||
std::uint32_t elementSize;
|
||||
std::uint32_t count;
|
||||
};
|
||||
|
||||
struct UpdateBuffer {
|
||||
std::uint32_t id;
|
||||
std::uint32_t elementSize;
|
||||
std::uint32_t count;
|
||||
};
|
||||
|
||||
struct DestroyBuffer {
|
||||
std::uint32_t id;
|
||||
};
|
||||
|
||||
// Triangles from buffers: indices and positions are required, the others optional.
|
||||
struct DrawTriangles {
|
||||
std::uint32_t indices;
|
||||
std::uint32_t positions;
|
||||
std::uint32_t normals;
|
||||
std::uint32_t texCoords;
|
||||
std::uint32_t colors;
|
||||
};
|
||||
|
||||
// Triangles from data in the payload, packed: indexCount 16-bit indices (padded to 4 bytes), then vertexCount
|
||||
// positions, and normals, texture coordinates and colors if present.
|
||||
struct DrawArrays {
|
||||
std::uint32_t vertexCount;
|
||||
std::uint32_t indexCount;
|
||||
std::uint32_t hasNormals;
|
||||
std::uint32_t hasTexCoords;
|
||||
std::uint32_t hasColors;
|
||||
};
|
||||
|
||||
static_assert(sizeof(Header) == 8 && sizeof(SetProjection) == 72 && sizeof(SetLight) == 28
|
||||
&& sizeof(DrawArrays) == 20);
|
||||
|
||||
inline constexpr std::size_t padded(std::size_t size) { return (size + 3) & ~std::size_t{3}; }
|
||||
|
||||
// Appends commands to a byte buffer.
|
||||
class Writer {
|
||||
public:
|
||||
// Appends a command and returns its payload, payloadSize bytes for the caller to fill.
|
||||
template <class Body>
|
||||
std::byte* add(Op op, const Body& body, std::size_t payloadSize = 0)
|
||||
{
|
||||
static_assert(sizeof(Body) % 4 == 0);
|
||||
const std::size_t size = sizeof(Body) + padded(payloadSize);
|
||||
const std::size_t start = bytes_.size();
|
||||
bytes_.resize(start + sizeof(Header) + size);
|
||||
const Header header{op, 0, static_cast<std::uint32_t>(size)};
|
||||
std::memcpy(bytes_.data() + start, &header, sizeof(Header));
|
||||
std::memcpy(bytes_.data() + start + sizeof(Header), &body, sizeof(Body));
|
||||
return bytes_.data() + start + sizeof(Header) + sizeof(Body);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const std::byte> bytes() const { return bytes_; }
|
||||
[[nodiscard]] std::size_t size() const { return bytes_.size(); }
|
||||
void clear() { bytes_.clear(); }
|
||||
|
||||
private:
|
||||
std::vector<std::byte> bytes_;
|
||||
};
|
||||
|
||||
// One command, as read back.
|
||||
class Command {
|
||||
public:
|
||||
Command(Op op, std::span<const std::byte> data) : op_(op), data_(data) {}
|
||||
|
||||
[[nodiscard]] Op op() const { return op_; }
|
||||
|
||||
// The body, if the command is long enough to hold one.
|
||||
template <class Body>
|
||||
[[nodiscard]] std::optional<Body> body() const
|
||||
{
|
||||
if (data_.size() < sizeof(Body)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
Body body;
|
||||
std::memcpy(&body, data_.data(), sizeof(Body));
|
||||
return body;
|
||||
}
|
||||
|
||||
template <class Body>
|
||||
[[nodiscard]] std::span<const std::byte> payload() const
|
||||
{
|
||||
return data_.size() < sizeof(Body) ? std::span<const std::byte>{} : data_.subspan(sizeof(Body));
|
||||
}
|
||||
|
||||
private:
|
||||
Op op_;
|
||||
std::span<const std::byte> data_;
|
||||
};
|
||||
|
||||
// Reads commands back from a byte buffer; stops at the end, or at a truncated command.
|
||||
class Reader {
|
||||
public:
|
||||
explicit Reader(std::span<const std::byte> bytes) : bytes_(bytes) {}
|
||||
|
||||
std::optional<Command> next()
|
||||
{
|
||||
if (bytes_.size() < sizeof(Header)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
Header header;
|
||||
std::memcpy(&header, bytes_.data(), sizeof(Header));
|
||||
if (bytes_.size() - sizeof(Header) < header.size) {
|
||||
return std::nullopt;
|
||||
}
|
||||
Command command(header.op, bytes_.subspan(sizeof(Header), header.size));
|
||||
bytes_ = bytes_.subspan(sizeof(Header) + header.size);
|
||||
return command;
|
||||
}
|
||||
|
||||
private:
|
||||
std::span<const std::byte> bytes_;
|
||||
};
|
||||
|
||||
} // namespace t5de::protocol
|
||||
Reference in new issue
Block a user