c2e95b4161
PiperOrigin-RevId: 956400340 Change-Id: Ibe049afe10eca85e5fb6d718a78863a641b864bb
129 lines
5.4 KiB
C++
129 lines
5.4 KiB
C++
// Copyright 2026 DeepMind Technologies Limited
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// This file defines the serialization format for the web viewer's browser
|
|
// client render payload containing the data needed so that the browser can
|
|
// render the scene using the following call:
|
|
//
|
|
// Render(model, data, perturb, camera, vis_options, width, height, extra_geoms)
|
|
//
|
|
// The arguments come from the Python process:
|
|
//
|
|
// * model : fetched once over HTTP as /model.mjb; its runtime-mutable
|
|
// parts (opt/vis/stat) are re-sent in the render state block.
|
|
// * data : streamed as the physics state vector (mjSTATE_INTEGRATION);
|
|
// the browser recomputes the rest via mj_setState/mj_forward.
|
|
// * width/height: the browser canvas size.
|
|
// * extra_geoms : optional variable-size kTagExtraGeoms block.
|
|
// * ... : the rest of the arguments are sent as a fixed-size block
|
|
//
|
|
// The payload (SerializeStatePayload) is a sequence of tagged blocks:
|
|
//
|
|
// [StatePayloadHeader][u32 tag][u32 size][payload]...
|
|
//
|
|
// The payload is serialized by Python, sent over the /state WebSocket, and
|
|
// parsed by the browser.
|
|
//
|
|
// TODO(matijak): Try shrinking the physics block: float32 (or quantized) values
|
|
// instead of doubles, and/or delta-encoding against the client's last-acked
|
|
// payload. The /state ack (web_server.py) tells the server which snapshot each
|
|
// client last applied, which is the baseline that delta compression needs. For
|
|
// 100humanoids.xml the payload is ~181 KB of doubles and dominates slow links.
|
|
|
|
#ifndef MUJOCO_PYTHON_EXPERIMENTAL_STUDIO_WEB_STATE_PAYLOAD_H_
|
|
#define MUJOCO_PYTHON_EXPERIMENTAL_STUDIO_WEB_STATE_PAYLOAD_H_
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <vector>
|
|
|
|
#include <mujoco/mujoco.h>
|
|
|
|
namespace mujoco::studio {
|
|
|
|
// "MJWS" as little-endian bytes. This magic constant identifies the
|
|
// StateServer WebSocket payload header and helps detect malformed or
|
|
// misrouted messages.
|
|
constexpr uint32_t kStatePayloadMagic =
|
|
'M' | ('J' << 8) | ('W' << 16) | ('S' << 24);
|
|
constexpr uint16_t kStatePayloadVersion = 1;
|
|
|
|
struct StatePayloadHeader {
|
|
uint32_t magic = kStatePayloadMagic;
|
|
uint16_t version = kStatePayloadVersion;
|
|
uint16_t nblocks = 0;
|
|
// CRC32 of the model's MJB bytes. When this changes, the browser must
|
|
// refetch /model.mjb before applying any further state.
|
|
uint32_t model_crc32 = 0;
|
|
};
|
|
static_assert(sizeof(StatePayloadHeader) == 12);
|
|
|
|
// Block tags. Readers must skip unknown tags.
|
|
enum StateBlockTag : uint32_t {
|
|
kTagPhysicsState = 1, // [i32 mjtState spec signature][mjtNum values...]
|
|
kTagRenderState = 2, // fixed-size block of kRenderStateSize bytes
|
|
kTagExtraGeoms = 3, // n x mjvGeom (n = size / sizeof(mjvGeom))
|
|
};
|
|
|
|
struct StateBlockHeader {
|
|
uint32_t tag = 0;
|
|
uint32_t size = 0;
|
|
};
|
|
static_assert(sizeof(StateBlockHeader) == 8);
|
|
|
|
// Fixed byte size of the render state block appended after physics state.
|
|
// These are plain C structs of int/float/double members whose total size is
|
|
// fixed, independent of the model and generally negligible compared to the size
|
|
// of the physics state
|
|
constexpr size_t kRenderStateSize =
|
|
sizeof(mjvCamera) + sizeof(mjvPerturb) + sizeof(mjvOption) +
|
|
sizeof(mjOption) + sizeof(mjVisual) + sizeof(mjStatistic) + mjNRNDFLAG;
|
|
|
|
// Maximum number of extra geoms serialized per frame. Bounds the shared
|
|
// memory buffer the StateServer allocates; WebViewer truncates longer lists.
|
|
constexpr uint32_t kMaxExtraGeoms = 1024;
|
|
|
|
// Upper bound of a serialized payload, used to size the StateServer's shared
|
|
// memory buffer. `physics_bytes` is mj_stateSize(...) * sizeof(mjtNum).
|
|
size_t MaxStatePayloadSize(size_t physics_bytes);
|
|
|
|
// Serialize the complete state payload sent over the state WebSocket.
|
|
std::vector<std::byte> SerializeStatePayload(
|
|
uint32_t model_crc32, int32_t physics_spec, const void* physics,
|
|
size_t physics_bytes, const mjvCamera& camera, const mjvPerturb& perturb,
|
|
const mjvOption& vis_options, const mjOption& opt, const mjVisual& vis,
|
|
const mjStatistic& stat, const std::vector<uint8_t>& render_flags,
|
|
const mjvGeom* extra_geoms, size_t extra_geom_count);
|
|
|
|
// Parsed view into a serialized payload. Pointers alias the input buffer and
|
|
// are NOT guaranteed to be aligned; so you must memcpy the data out before use.
|
|
struct StatePayloadView {
|
|
uint32_t model_crc32 = 0;
|
|
int32_t physics_spec = 0;
|
|
const std::byte* physics = nullptr;
|
|
size_t physics_bytes = 0;
|
|
const std::byte* render_state = nullptr; // kRenderStateSize bytes when non-null
|
|
const std::byte* extra_geoms = nullptr; // extra_geom_count * sizeof(mjvGeom)
|
|
size_t extra_geom_count = 0;
|
|
};
|
|
|
|
// Parses a payload produced by SerializeStatePayload. Returns false if the
|
|
// buffer is malformed (bad magic/version or out-of-bounds block). Blocks
|
|
// with unknown tags are skipped.
|
|
bool ParseStatePayload(const void* data, size_t size, StatePayloadView* out);
|
|
|
|
} // namespace mujoco::studio
|
|
|
|
#endif // MUJOCO_PYTHON_EXPERIMENTAL_STUDIO_WEB_STATE_PAYLOAD_H_
|