From ce025f0ec3fb22243466e42e7b1da93dc15eacd7 Mon Sep 17 00:00:00 2001 From: Anas Date: Sat, 20 Jun 2026 11:05:31 -0400 Subject: [PATCH] render: skip textures the classic renderer can't upload KTX textures crash the classic OpenGL renderer and the Python viewer with "Number of channels not supported: 1": mjCTexture::LoadKTX stores the file as an opaque single-channel blob for the Filament renderer, and mjr_uploadTexture aborts via mju_error because the classic path only handles 3- or 4-channel textures. Skip textures the classic renderer can't upload (nchannel not 3 or 4) with a warning instead of aborting, so the model still loads and the other textures render. LoadKTX and the Filament path are unchanged. The guard also covers the cube/skybox path, which previously over-read a single-channel blob as RGB. Add a render_test.py regression test that builds a model with a KTX texture and constructs an MjrContext; it aborts before this change and passes after. Addresses #3343. --- python/mujoco/render_test.py | 30 +++++++++++++++++++++++++++++ src/render/classic/render_context.c | 14 +++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/python/mujoco/render_test.py b/python/mujoco/render_test.py index 2df0a813..ef79a0cf 100644 --- a/python/mujoco/render_test.py +++ b/python/mujoco/render_test.py @@ -14,6 +14,8 @@ # ============================================================================== """Tests for MuJoCo Python rendering.""" +import os + from absl.testing import absltest import mujoco import numpy as np @@ -104,6 +106,34 @@ class MuJoCoRenderTest(absltest.TestCase): context.free() context.free() + def test_ktx_texture_does_not_crash_classic_renderer(self): + # KTX textures are stored as opaque single-channel blobs for the Filament + # renderer; mjCTexture::LoadKTX does not decode them. The classic renderer + # must skip such textures with a warning rather than aborting in + # mjr_uploadTexture (https://github.com/google-deepmind/mujoco/issues/3343). + ktx_path = os.path.join(self.create_tempdir().full_path, 'texture.ktx') + with open(ktx_path, 'wb') as f: + # LoadKTX stores the raw payload as a 1-channel blob without decoding, so + # the contents do not matter; start with the KTX 2.0 identifier for realism. + f.write(b'\xabKTX 20\xbb\r\n\x1a\n' + bytes(64)) + + model = mujoco.MjModel.from_xml_string(f""" + + + + + + + + + """) + self.assertEqual(model.tex_nchannel[0], 1) + + # Building the render context uploads all textures; before the fix this + # aborted with "Number of channels not supported: 1". It must not raise now. + context = mujoco.MjrContext(model, mujoco.mjtFontScale.mjFONTSCALE_150) + context.free() + def test_mjrrect_repr(self): rect = mujoco.MjrRect(1, 2, 3, 4) rect_repr = repr(rect) diff --git a/src/render/classic/render_context.c b/src/render/classic/render_context.c index c7a5dd0b..3c6c8025 100644 --- a/src/render/classic/render_context.c +++ b/src/render/classic/render_context.c @@ -1365,6 +1365,16 @@ void mjr_uploadTexture(const mjModel* m, const mjrContext* con, int texid) { int w = m->tex_width[texid]; float plane[4]; + // the classic renderer only uploads 3- (RGB) or 4-channel (RGBA) textures. KTX + // and other encoded textures are single-channel blobs for the Filament renderer + // (see LoadKTX); skip them with a warning instead of aborting (issue #3343). + if (m->tex_nchannel[texid] != 3 && m->tex_nchannel[texid] != 4) { + mju_warning("texture %d: classic renderer can't upload %d-channel data, " + "skipping (KTX/encoded textures need the Filament renderer)", + texid, m->tex_nchannel[texid]); + return; + } + // 2D texture if (m->tex_type[texid] == mjTEXTURE_2D) { // OpenGL settings @@ -1390,11 +1400,9 @@ void mjr_uploadTexture(const mjModel* m, const mjrContext* con, int texid) { if (m->tex_nchannel[texid] == 3) { type = GL_RGB; internaltype = (m->tex_colorspace[texid] == mjCOLORSPACE_SRGB) ? GL_SRGB8_EXT : GL_RGB; - } else if (m->tex_nchannel[texid] == 4) { + } else { // 4 channels, guaranteed by the channel check at the top of the function type = GL_RGBA; internaltype = (m->tex_colorspace[texid] == mjCOLORSPACE_SRGB) ? GL_SRGB8_ALPHA8_EXT : GL_RGBA; - } else { - mju_error("Number of channels not supported: %d", m->tex_nchannel[texid]); } glTexImage2D(GL_TEXTURE_2D, 0, internaltype, m->tex_width[texid],