Introduce mjpEncoder plugin architecture
Add a new mjpEncoder plugin type mirroring the existing mjpDecoder pattern. Encoders serialize an mjSpec + mjModel to an mjResource for a given format. New API functions: - mjp_registerEncoder: globally register an encoder - mjp_defaultEncoder: zero-initialize an encoder struct - mjp_findEncoder: look up an encoder by filename extension or content type The mjfEncode callback takes (mjSpec*, mjModel*, mjVFS*, mjResource*) and returns 0 on success. Writing to mjResource keeps symmetry with the decoder reading from mjResource and leaves the door open for writable resource providers. PiperOrigin-RevId: 889187898 Change-Id: I180771b2255b91dea188ac5e2cdc3a8f0fb85364
This commit is contained in:
committed by
Copybara-Service
parent
2d33b50243
commit
f5d3ce3451
@@ -0,0 +1,134 @@
|
||||
// Copyright 2025 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
|
||||
//
|
||||
// http://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.
|
||||
|
||||
// Tests for encoder plugins.
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <mujoco/mjmodel.h>
|
||||
#include <mujoco/mjplugin.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "test/fixture.h"
|
||||
|
||||
namespace mujoco {
|
||||
namespace {
|
||||
|
||||
struct FakeEncoderOutput {
|
||||
int nbody;
|
||||
int ngeom;
|
||||
int njnt;
|
||||
char resource_name[512];
|
||||
};
|
||||
|
||||
int FakeEncode(const mjSpec* s, const mjModel* m, const mjVFS* vfs,
|
||||
mjResource* resource) {
|
||||
auto* output = new FakeEncoderOutput;
|
||||
output->nbody = m->nbody;
|
||||
output->ngeom = m->ngeom;
|
||||
output->njnt = m->njnt;
|
||||
std::snprintf(output->resource_name, sizeof(output->resource_name), "%s",
|
||||
resource->name);
|
||||
resource->data = output;
|
||||
return sizeof(FakeEncoderOutput);
|
||||
}
|
||||
|
||||
void CloseResource(mjResource* resource) {
|
||||
delete static_cast<FakeEncoderOutput*>(resource->data);
|
||||
delete resource;
|
||||
}
|
||||
|
||||
mjpEncoder FakeEncoder() {
|
||||
mjpEncoder encoder;
|
||||
mjp_defaultEncoder(&encoder);
|
||||
encoder.content_type = "model/fakeformat";
|
||||
encoder.extension = ".fakeformat|.alsoFakeFormat";
|
||||
encoder.encode = FakeEncode;
|
||||
encoder.close_resource = CloseResource;
|
||||
return encoder;
|
||||
}
|
||||
|
||||
using EncoderPluginTest = MujocoTest;
|
||||
|
||||
TEST_F(EncoderPluginTest, RegisterAndFindByExtension) {
|
||||
mjpEncoder encoder = FakeEncoder();
|
||||
mjp_registerEncoder(&encoder);
|
||||
|
||||
const mjpEncoder* found = mjp_findEncoder("output.fakeformat", nullptr);
|
||||
ASSERT_THAT(found, testing::NotNull());
|
||||
EXPECT_EQ(found->encode, FakeEncode);
|
||||
}
|
||||
|
||||
TEST_F(EncoderPluginTest, FindByAlternateExtension) {
|
||||
const mjpEncoder* found = mjp_findEncoder("output.alsoFakeFormat", nullptr);
|
||||
ASSERT_THAT(found, testing::NotNull());
|
||||
EXPECT_EQ(found->encode, FakeEncode);
|
||||
}
|
||||
|
||||
TEST_F(EncoderPluginTest, FindByContentType) {
|
||||
const mjpEncoder* found = mjp_findEncoder(nullptr, "model/fakeformat");
|
||||
ASSERT_THAT(found, testing::NotNull());
|
||||
EXPECT_EQ(found->encode, FakeEncode);
|
||||
}
|
||||
|
||||
TEST_F(EncoderPluginTest, FindUnknownExtensionReturnsNull) {
|
||||
const mjpEncoder* found = mjp_findEncoder("output.unknown", nullptr);
|
||||
EXPECT_THAT(found, testing::IsNull());
|
||||
}
|
||||
|
||||
TEST_F(EncoderPluginTest, DefaultEncoderIsZeroed) {
|
||||
mjpEncoder encoder;
|
||||
mjp_defaultEncoder(&encoder);
|
||||
EXPECT_EQ(encoder.content_type, nullptr);
|
||||
EXPECT_EQ(encoder.extension, nullptr);
|
||||
EXPECT_EQ(encoder.encode, nullptr);
|
||||
}
|
||||
|
||||
TEST_F(EncoderPluginTest, EncodeModel) {
|
||||
mjSpec* spec = mj_makeSpec();
|
||||
mjsBody* world = mjs_findBody(spec, "world");
|
||||
mjsBody* body = mjs_addBody(world, nullptr);
|
||||
mjsGeom* geom = mjs_addGeom(body, nullptr);
|
||||
geom->size[0] = 1.0;
|
||||
geom->size[1] = 1.0;
|
||||
geom->size[2] = 1.0;
|
||||
|
||||
mjModel* model = mj_compile(spec, nullptr);
|
||||
ASSERT_THAT(model, testing::NotNull());
|
||||
|
||||
const mjpEncoder* found = mjp_findEncoder("output.fakeformat", nullptr);
|
||||
ASSERT_THAT(found, testing::NotNull());
|
||||
|
||||
mjResource resource = {};
|
||||
resource.name = const_cast<char*>("output.fakeformat");
|
||||
|
||||
int result = found->encode(spec, model, nullptr, &resource);
|
||||
EXPECT_GT(result, 0);
|
||||
|
||||
auto* output = static_cast<FakeEncoderOutput*>(resource.data);
|
||||
ASSERT_THAT(output, testing::NotNull());
|
||||
EXPECT_EQ(output->nbody, 2);
|
||||
EXPECT_EQ(output->ngeom, 1);
|
||||
EXPECT_EQ(output->njnt, 0);
|
||||
EXPECT_STREQ(output->resource_name, "output.fakeformat");
|
||||
|
||||
delete output;
|
||||
mj_deleteModel(model);
|
||||
mj_deleteSpec(spec);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
Reference in New Issue
Block a user