Geometry extraction
Can PyMOL’s already-computed render geometry be pulled out of the C++ backend and shipped to a browser, so the browser draws it instead of recomputing representations from atoms? This is the inventory that answered that, read out ofpackages/engine/. Every claim carries a file:line.
Where something could not be confirmed by reading code, the text says so.
Where the port stands. Yes, and it is built. _cmd.web_get_rep_geometry is the accessor of §6
(packages/engine/layer4/Cmd.cpp:6471, implementation packages/engine/layer4/CmdWebGeometry.cpp);
it is served by packages/bridge/tenmol_bridge/render/modeg.py, typed in
packages/protocol/src/geometry.ts, and drawn by packages/viewport/src/modeG/. The
raster fallback for everything Mode G cannot draw is packages/viewport/src/modeP/, with the
per-rep switch in packages/viewport/src/renderPolicy.ts. Measurements are in
docs/spikes/geometry.md and docs/spikes/geometry-accessor.md.
0. TL;DR
- YES, PyMOL keeps a complete, CPU-resident, GL-free description of every representation’s geometry.
It lives in two forms: (a) plain
std::vector<float>triangle soup for surfaces (packages/engine/layer2/RepSurface.cpp:71-83), and (b) CGO buffers — a flatfloat*bytecode with a documented opcode set — for every other rep (packages/engine/layer1/CGO.h:82-270,packages/engine/layer1/CGO.h:765-773). - NO, none of that is exposed to Python today for molecular reps. The only Python-visible geometry
exports are
cmd.get_vrml / get_collada / get_idtf / get_povray / get_mtl_obj / get_gltf(packages/engine/modules/pymol/exporting.py:1012-1019), and all six of them route through the ray tracer (packages/engine/layer1/SceneRay.cpp:336-491), not through the GL/CGO buffers. They are ASCII, per-primitive, camera-space, lossy, and multi-second. None is usable as an interactive feed. - The exception is surfaces:
cache_mode >= 2already serializes the surface triangle mesh(N, V, VN, NT, T, S)into a Python tuple that lands inpymol._cache(packages/engine/layer2/RepSurface.cpp:3217-3229,packages/engine/layer2/RepSurface.cpp:4559-4574,packages/engine/modules/pymol/internal.py:101). That is a real, existing, zero-new-C++ path to a surface mesh — but Python lists of boxed floats, and no colors. - The answer that shipped is a new CPython accessor that walks
CoordSet::Rep[cRepCnt](packages/engine/layer2/CoordSet.h:107), runs the existing CPU tessellatorsCGOSimplify()(packages/engine/layer1/CGO.cpp:4444) +CGOCombineBeginEnd()(packages/engine/layer1/CGO.cpp:1539), and hands back the resultingCGO_DRAW_ARRAYSfloat blocks as rawPyBytesusing the primitive that already exists forpse_binary_dump:PConvFloatArrayToPyList(f, l, dump_binary=true)→PyBytes_FromStringAndSize(packages/engine/layer1/PConv.cpp:971-977). That block is literally a three.jsBufferGeometry. - Visual parity is achievable for the interactive (GL) viewport and NOT for ray tracing. See §8.
1. The two geometry pipelines inside PyMOL
PyMOL has three renderers behind oneRep::render(RenderInfo*) virtual (packages/engine/layer1/Rep.h:198):
The critical structural fact, verified in
packages/engine/layer2/RepSphere.cpp:149-207: renderCGO is generated
lazily inside render() and only when G->HaveGUI && G->ValidContext (line 164). The
build-time artifact (primitiveCGO) is generated in RepSphereNew (packages/engine/layer2/RepSphere.cpp:523-582)
with no GL involvement at all.
Same shape for cartoon: I->preshader is built in RepCartoonNew
(packages/engine/layer2/RepCartoon.cpp:4287-4300) with no GL, then consumed and destroyed by
RepCartoonCGOGenerate() at first GL render (packages/engine/layer2/RepCartoon.cpp:263-274, dispose at :240).
Note the salvage path RepCartoon::disposePreshaderCGO() (packages/engine/layer2/RepCartoon.cpp:83-89): if
ray is null the preshader is moved into ray rather than freed — so in a ray-capable build the
CPU cartoon geometry survives GL upload.
Where the CPU copy is lost
OptimizeVertsToVBONotIndexed() builds an interleaved std::vector<float> vertexValsVec(tot) on the
stack (packages/engine/layer1/CGO.cpp:2947-2948), fills it via CGOProcessCGOtoArrays() (:2968), uploads with
SetVertexBufferData() (:2998), and lets the vector die at scope exit. packages/engine/layer1/CGO.h:183-186
states this explicitly: “the geometry data is not kept in the CGO object for VBO objects (only on the
card)” — which is why CGO_BOUNDING_BOX exists at all.
Conclusion: do not try to read back VBOs. Read the pre-VBO CPU CGO.
The point in the pipeline where CPU geometry is complete
SceneUpdate(G, force) (packages/engine/layer1/Scene.cpp:4664) drives obj->update() for every object
(packages/engine/layer1/Scene.cpp:4757-4760 single-threaded branch, :4745-4751 threaded branch), which reaches
Rep::update() → Rep::rebuild() → fNew(cs, state) (packages/engine/layer1/Rep.cpp:42-57, 65-101).
After SceneUpdate returns, every Rep’s CPU geometry is final and no GL context was required.
cmd.rebuild() (packages/engine/modules/pymol/viewing.py:1837-1866 → _cmd.rebuild) and
cmd.refresh() (packages/engine/modules/pymol/viewing.py:1750-1772) are the Python triggers.
2. Per-Rep geometry inventory (what each Rep actually produces)
Enumerated fromenum cRep_t (packages/engine/layer1/Rep.h:48-74). Reps live in CoordSet::Rep[cRepCnt] with an
Active[cRepCnt] flag array (packages/engine/layer2/CoordSet.h:107-108).
cRepSurface (2) — packages/engine/layer2/RepSurface.cpp:59-101
The richest and easiest target. All arrays are std::vector members, CPU-resident, never freed
after GL upload:
T is produced by TrianglePointsToSurface() (packages/engine/layer2/RepSurface.cpp:4035-4036, declared
packages/engine/layer0/Triangle.h:25-27). Colors VC/VA/RC are (re)computed in RepSurface::recolor()
(packages/engine/layer2/RepSurface.cpp:2363). Per-triangle culling uses
visibility_test(proximity, Vis, &T[3i]) (packages/engine/layer2/RepSurface.cpp:209-216) — the client MUST replicate
this or it will draw hidden surface patches.
Direct three.js mapping: V → position, VN → normal, VC+VA → color (RGBA),
T → index buffer, VAO → custom attribute, AT → picking id attribute. Nothing to recompute.
cRepSphere (1) — packages/engine/layer2/RepSphere.h:29-40
primitiveCGO (:38) holds CGO_SPHERE ops (center xyz + radius) interleaved with CGO_COLOR,
CGO_ALPHA, CGO_PICK_COLOR (packages/engine/layer2/RepSphere.cpp:290-313). Built GL-free at
packages/engine/layer2/RepSphere.cpp:523-582. spheroidCGO (:39) is the anisotropic-spheroid variant built by
RepSphereGeneratespheroidCGO() (packages/engine/layer2/RepSphere.cpp:366) which emits real triangles.
GL path forks on sphere_mode (packages/engine/layer2/RepSphereGenerate.cpp:24-102):
- mode 9 (default) → GLSL impostor quads,
CGOOptimizeSpheresToVBONonIndexed(packages/engine/layer1/CGO.cpp:4357) - mode 0 / cube / tetrahedron → real triangles via
CGOSimplify(primitiveCGO, 0, sphere_quality)(packages/engine/layer2/RepSphereGenerate.cpp:36) - modes 1,2,3,6,7,8 → point sprites,
CGOConvertSpheresToPoints(packages/engine/layer2/RepSphereGenerate.cpp:75)
cRepCyl (0, sticks) — packages/engine/layer2/RepCylBond.cpp:39-49
primitiveCGO (:47) built GL-free (packages/engine/layer2/RepCylBond.cpp:635 alloc, :903 CGOStop). Contains
CGO_SHADER_CYLINDER / CGO_SHADER_CYLINDER_WITH_2ND_COLOR / CGO_SPHERE (for zero-order bond
dots, :798) plus color/alpha/pickcolor. RepCylinder() helper documented at :51-68.
cRepCartoon (5) — packages/engine/layer2/RepCartoon.cpp:65-92
preshader (:77) is the CPU CGO. Built by GenerateRepCartoonCGO()
(packages/engine/layer2/RepCartoon.cpp:4287-4290), then immediately normalized with CGOCombineBeginEnd()
(packages/engine/layer2/RepCartoon.cpp:4292-4294) — meaning cartoon geometry is already in CGO_DRAW_ARRAYS
interleaved-array form before anything GL happens. The actual extrusion emits
GL_TRIANGLE_STRIP / GL_TRIANGLE_FAN / GL_LINE_STRIP blocks in packages/engine/layer1/Extrude.cpp
(:839, :932, :1036, :1129, :1363, :1450, :1483, :1569, :1631, :1671, :1793, :1912, :1987, :2047, :2128, :2153)
plus cgo::draw::shadercylinder2ndcolor for nucleic-acid-as-cylinders (packages/engine/layer1/Extrude.cpp:1219),
plus CGOSphere for ring centers (packages/engine/layer2/RepCartoon.cpp:1295) and GL_TRIANGLES for rings (:1346).
cRepRibbon (6) — packages/engine/layer2/RepRibbon.cpp:37-50: primitiveCGO (:48), CPU.
cRepLine (7, wire) — packages/engine/layer2/RepWireBond.cpp:34-45: primitiveCGO (:43), CPU. CGO_LINE/CGO_SPLITLINE.
cRepNonbonded (11) — packages/engine/layer2/RepNonbonded.cpp:33-44: primitiveCGO (:41), CPU.
cRepNonbondedSphere (4) — packages/engine/layer2/RepNonbondedSphere.cpp:35-44: primitiveCGO (:43), CPU.
cRepEllipsoid (19) — packages/engine/layer2/RepEllipsoid.cpp:33-44: ray (:41) and std (:42) CGOs. CGO_ELLIPSOID → CGOSimpleEllipsoid in CGOSimplify (packages/engine/layer1/CGO.cpp:4535).
cRepMesh (8) — packages/engine/layer2/RepMesh.cpp:39-63: raw pymol::vla<float> V (:51), N strip lengths (:49), NTot (:50), VC colors (:52), Dot/NDot (:53-54). CPU, permanent.
cRepDot (9) — packages/engine/layer2/RepDot.h:26-45: V, VC, A (area), VN, T, F, Atom raw float/int arrays (:34-42). CPU, permanent.
cRepDash (10) — packages/engine/layer2/RepDistDash.cpp:40-55: raw float* V (:48), N (:49). CPU.
cRepAngle (17) — packages/engine/layer2/RepAngle.cpp:36-50: pymol::vla<float> V (:44). CPU.
cRepDihedral (18) — packages/engine/layer2/RepDihedral.cpp:35-48: float* V (:43). CPU.
cRepLabel (3) — packages/engine/layer2/RepLabel.cpp:90-104: labelV vector + lexer indices L (:98-99).
Geometry is texture-atlas quads, not mesh. Glyph bitmaps are reachable CPU-side via
CharacterGetPixmapBuffer() (packages/engine/layer1/Character.cpp:122-130) and metrics via
CharacterGetGeometry / CharacterGetWidth / CharacterGetHeight / CharacterGetAdvance
(packages/engine/layer1/Character.h:80-85). Ops are CGO_DRAW_LABEL (packages/engine/layer1/CGO.h:224-225) and
CGO_DRAW_LABELS (:228).
cRepDistLabel — packages/engine/layer2/RepDistLabel.cpp:41-56: same texture-quad story.
cRepCGO (13) — user CGOs. ObjectCGOState::origCGO (packages/engine/layer2/ObjectCGO.h:25) is CPU and
already round-trips to Python — see §3.
cRepCallback (14) — arbitrary Python glDraw* callbacks. Not extractable, by definition.
cRepVolume (20) / cRepSlice (16) — 3D-texture ray-marching (packages/engine/data/shaders/volume.fs),
not triangle geometry. Out of scope for a mesh feed.
cRepCell (12) / cRepExtent (15) — trivial line boxes.
3. CGO — the existing serialization format (this is the wire format, already)
class CGO (packages/engine/layer1/CGO.h:765-773) is a float* op array of length c. Opcodes are
#defines at packages/engine/layer1/CGO.h:82-270, each with a fixed _SZ payload length. The Python-side mirror
of the opcode table already exists at packages/engine/modules/pymol/cgo.py:21-79.
Key opcodes for a geometry feed:
CGO_DRAW_ARRAYS payload layout — the money shot
struct cgo::draw::arrays (packages/engine/layer1/CGO.h:338-355) carries {mode, arraybits, narrays, nverts} plus
a heap block. CGOCombineBeginEnd() lays that block out at packages/engine/layer1/CGO.cpp:1645-1672:
VERTEX_POS_SIZE 3 / VERTEX_COLOR_SIZE 4 (packages/engine/layer0/ShaderMgr.h:430-431),
VERTEX_NORMAL_SIZE 3 (packages/engine/layer1/CGO.cpp:55), VERTEX_PICKCOLOR_SIZE = 1 + 2 and
VERTEX_ACCESSIBILITY_SIZE 1 (packages/engine/layer1/CGO.cpp:60-65). Array bit flags at packages/engine/layer1/CGO.h:272-277.
This is a three.js BufferGeometry with zero transformation. mode is a raw GL enum
(GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_LINES, GL_LINE_STRIP, GL_POINTS —
enumerated in packages/engine/layer1/CGO.h:68-72 and mirrored in packages/engine/modules/pymol/cgo.py:22-27).
The two CPU tessellators (no GL required)
CGOSimplify(const CGO*, est, sphere_quality, stick_round_nub)—packages/engine/layer1/CGO.cpp:4444. Converts every analytic primitive into triangles:CGO_SPHERE→CGOSimpleSphere(:4533) usingG->Sphere->Sphere[sphere_quality];CGO_SHADER_CYLINDER/CGO_CYLINDER/CGO_SAUSAGE/CGO_CUSTOM_CYLINDER/CGO_CUSTOM_CYLINDER_ALPHA→CGOSimpleCylinder(:4474-4530);CGO_CONE→CGOSimpleCone(:4511);CGO_ELLIPSOID→CGOSimpleEllipsoid(:4536);CGO_QUADRIC→CGOSimpleQuadric(:4539). It also foldsBEGIN/…/ENDblocks intoCGO_DRAW_ARRAYS(:4550-4620).CGOCombineBeginEnd(const CGO*, est, do_not_split_lines)—packages/engine/layer1/CGO.cpp:1539. BEGIN/VERTEX/END →CGO_DRAW_ARRAYSonly (no primitive tessellation).
CGO ↔ Python already exists (for ObjectCGO only)
CGOAsPyList(CGO*)(packages/engine/layer1/CGO.cpp:289-297) →[len, flat_float_list], built byCGOArrayAsPyList(packages/engine/layer1/CGO.cpp:241-287). It explicitly handlesCGO_DRAW_ARRAYS(:255-265), flattening{mode, arraybits, narrays, nverts}+ the data block. So the serializer for the exact format we want already exists in the tree.CGONewFromPyList()(packages/engine/layer1/CGO.cpp:392) is the inverse.- Reached from Python only via session save of
ObjectCGO(packages/engine/layer2/ObjectCGO.cpp:43, 151,packages/engine/layer3/Executive.cpp:5407),GadgetSet(packages/engine/layer2/GadgetSet.cpp:191, 197), andCoordSet::SculptCGO(packages/engine/layer2/CoordSet.cpp:392-394). - Import side:
cmd.load_cgo(list_of_floats, name, state)(packages/engine/modules/pymol/importing.py:307-324) →ObjectCGOFromFloatArray(packages/engine/layer2/ObjectCGO.h:47) /CGOFromFloatArray(packages/engine/layer1/CGO.h:1109).
cmd.get_session() does NOT contain molecular rep geometry. CoordSetAsPyList
(packages/engine/layer2/CoordSet.cpp:364-416) writes coords, atom index maps, settings, symmetry, and SculptCGO —
no Rep[]. Verified by reading the whole function.
Binary blob primitive already exists
PConvFloatArrayToPyList(const float*, int, bool dump_binary) returns
PyBytes_FromStringAndSize(reinterpret_cast<const char*>(f), l * sizeof(float)) when
dump_binary is true (packages/engine/layer1/PConv.cpp:971-977). Used today by pse_binary_dump
(packages/engine/layer2/CoordSet.cpp:371-372, 377). This is exactly the zero-copy-ish primitive a binary geometry
accessor needs — no new serialization machinery to invent.
Zero-copy numpy precedent: CoordSetAsNumPyArray(cs, copy) (packages/engine/layer2/CoordSet.cpp:326-361) —
with copy == 0 it returns PyArray_SimpleNewFromData(2, dims, typenum, cs->Coord.data())
(:357), a numpy view onto live C++ memory. Exposed as cmd.get_coordset(name, state, copy)
(packages/engine/layer4/Cmd.cpp:2033-2056, table entry packages/engine/layer4/Cmd.cpp:6457).
4. Complete inventory of existing Python-exposed export paths
Registered insavefunctions at packages/engine/modules/pymol/exporting.py:988-1020.
Shared cost model for all six geometry exporters. Every one calls
SceneRay(G, 0, 0, mode, …) (packages/engine/layer1/SceneRay.cpp:88), which:
SceneUpdate(G, false)(:152) — rebuild all reps if dirty,- allocates a fresh
CRay(:213) and re-runsobj->render(&info)for every object withinfo.rayset (:284-323), re-emitting every primitive from scratch, RayExpandPrimitives()(packages/engine/layer1/Ray.cpp:561) — allocates vertex/normal/radius VLAs sized to the full primitive count,RayTransformFirst()(packages/engine/layer1/Ray.cpp:822) — transforms all vertices byModelView(skipped only whengeometry_export_mode == 1,packages/engine/layer1/Ray.cpp:876-881),sprintfeverything into achar*VLA,RayFree(ray)(:492) — throws the whole thing away.
CPrimitive is 172 bytes
(packages/engine/layer1/Basis.h:68-83) and the header comment notes ~6.5M primitives/GB. Every call rebuilds
everything.
5. Which existing path is closest to usable, and what it costs
5a. Winner for surfaces (today, no C++ changes): the surface cache
cmd.cache('enable') sets cache_mode=2 (packages/engine/modules/pymol/exporting.py:87-88). Then, in
RepSurfaceNew, after SurfaceJobRun() succeeds and cache_mode > 1, PyMOL calls
SurfaceJobResultAsTuple() and PCacheSet() (packages/engine/layer2/RepSurface.cpp:4566-4574).
SurfaceJobResultAsTuple (packages/engine/layer2/RepSurface.cpp:3217-3229) builds:
PCacheSet (packages/engine/layer1/P.cpp:1349-1376) stores it as entry[3] on a list
[size, hash_list, input_tuple, output_tuple, access_count, timestamp] appended to
pymol._cache by cmd._cache_set (packages/engine/modules/pymol/internal.py:101-125). It is directly readable:
VC colors, VA alpha, Vis
visibility, VAO occlusion, AT atom mapping — all of which live on the RepSurface and are not
part of the cached SurfaceJob result. Missing colors is fatal for a viewer, so this is a
prototype-grade path only.
Cost: PConvToPyObject(std::vector<float>) (packages/engine/layer1/PConv.h:251-259) builds a PyList of boxed
PyFloats — one object per float. A 500k-vertex surface = 3M PyFloats ≈ 70+ MB and hundreds of ms
just to materialize, plus the same again to JSON/msgpack it. Only acceptable once per surface build,
never per frame.
5b. Winner for everything else (today): cmd.get_vrml(2)
Highest-fidelity general export. Still one full SceneRay per call, ASCII, camera-space, cones
dropped, labels dropped, ellipsoids dropped.
5c. Per-frame cost verdict
None of the existing paths can run per frame. Rough shape of the cost for a modest 3k-atom cartoon+sticks scene:
Conclusion for the architecture: geometry must NOT be re-derived per frame at all. The client
should hold the mesh and only receive (a) camera/view updates via
cmd.get_view()
(packages/engine/modules/pymol/viewing.py:634-679, 18 floats: 3x3 rotation, camera-space origin, model-space origin,
front/back clip, ortho-flag/FOV) and (b) invalidation events when a rep is rebuilt.
6. The new C++/Python accessor
This section is the design that became_cmd.web_get_rep_geometry
(packages/engine/layer4/Cmd.cpp:6471, implementation packages/engine/layer4/CmdWebGeometry.cpp,
wrapped in /* tenmol web client -- BEGIN/END */ sentinels).
6a. Design
One new_cmd method, web_get_rep_geometry(G, object_name, state, rep_id, flags), registered in
the method table alongside the existing get_* entries (packages/engine/layer4/Cmd.cpp:6466-6478).
It follows the shape of CmdGetCoordSetAsNumPy (packages/engine/layer4/Cmd.cpp:2033-2056):
API_SETUP_ARGS → APIEnterBlocked → ExecutiveGetCoordSet
(packages/engine/layer3/Executive.h:869) → build result → APIExitBlocked.
Resolution path (all already public):
SceneUpdate(G, true) (packages/engine/layer1/Scene.cpp:4664) first, or require the caller to
have called cmd.rebuild() — otherwise Rep[] may be stale or null.
6b. Return payload
Adict per rep, with PyBytes blobs, not lists:
6c. Implementation, entirely from existing functions
sphere_mode 9 / render_as_cylinders), skip CGOSimplify and instead
walk the CGO_SPHERE / CGO_SHADER_CYLINDER / CGO_SHADER_CYLINDER_WITH_2ND_COLOR ops directly
and pack them into instance buffers. That mirrors what
CGOOptimizeSpheresToVBONonIndexed (packages/engine/layer1/CGO.cpp:4357-4394) does for GL.
6d. Bulk / streaming variant
Add_cmd.get_scene_geometry(G, flags) that iterates G->Scene->Obj the same way SceneRay does
(packages/engine/layer1/SceneRay.cpp:284-323) and returns a list of the per-rep dicts above, plus per-object
transforms from cmd.get_object_matrix (packages/engine/layer4/Cmd.cpp:6480) — required because
ObjectMolecule::render applies ObjectStatePushAndApplyMatrix when matrix_mode != 0
(packages/engine/layer2/ObjectMolecule.cpp:11265-11269).
6e. Invalidation events
The bridge must know when to re-pull.Rep::invalidate(cRepInv_t) (packages/engine/layer1/Rep.h:199) and the
cRepInv_t ladder (packages/engine/layer1/Rep.h:133-184) are the natural hook:
cRepInvColor(15) → re-send colors only; cRepInvRep(35)/cRepInvAll(100) → re-send everything.
There is no existing Python callback for rep invalidation — I grepped and found none. This must
be added (a PYOBJECT_CALLMETHOD into G->P_inst->cmd, following the pattern of
call_raw_image_callback at packages/engine/layer1/Scene.cpp:4020-4051).
6f. What it cost
- Surface accessor: memcpy of 8 existing vectors.
- CGO accessor:
CGOSimplify+ iterate +PyBytes, a variant of the existingCGOArrayAsPyList(packages/engine/layer1/CGO.cpp:241-287). - Invalidation: no callback was added.
Rep::update()runs on worker threads whenasync_buildsis on (packages/engine/layer1/Scene.cpp:4740-4757), so a callback would have had to avoid touching Python from a non-GIL thread. Instead the four change counters of_cmd.web_get_versionsare polled — seedocs/cmd-api-rpc.md§8.5 andpackages/bridge/tenmol_bridge/state/repversions.py.
7. Shader parity assets already in the repo
packages/engine/data/shaders/ contains PyMOL’s GLSL, and the preprocessor already has WebGL/ES2 code paths:
m_shaderPreprocessor.setVar("PURE_OPENGL_ES_2"/"PYMOL_WEBGL"/"PYMOL_WEBGL_IOS", true)
under #ifdef PURE_OPENGL_ES_2 (packages/engine/layer0/ShaderMgr.cpp:604-607), with
packages/engine/data/shaders/webgl_header.vs and packages/engine/data/shaders/webgl_header.fs supplying the uniform block
(g_NormalMatrix, g_ModelViewMatrix, g_ProjectionMatrix, g_Fog_end, g_Fog_scale).
Programs registered (packages/engine/layer0/ShaderMgr.cpp:617-658): bg, label, default, surface, line,
screen, connector, bezier, cylinder, sphere, ramp, oit, trilines.
Shared includes: compute_color_for_light.fs (101 lines), compute_fog_color.fs (52),
anaglyph_header.fs (161), call_compute_color_for_light.fs.
sphere.fs (94 lines) is a genuine ray-sphere impostor writing gl_FragDepth — requires
GL_EXT_frag_depth on WebGL1, native on WebGL2 (packages/engine/data/shaders/sphere.fs:1-3, 55-60).
cylinder.fs is 187 lines of impostor math. These are directly portable with matched uniform names, and are what
packages/viewport/src/modeG/materials/ and packages/viewport/src/shaders/ are derived from. ~1840 lines of GLSL total across
all shaders — this is the single biggest lever for visual parity and it is copy-adaptable, not
reinventable.
8. Verdict
Can a browser client reach visual parity with PyMOL by drawing PyMOL’s own geometry? For the interactive OpenGL viewport: yes. The mesh data is exact (it is PyMOL’s data), the shaders are portable, and the camera model is fully specified bycmd.get_view(). What remains is
a long tail of small mismatches, enumerated below.
For ray-traced output: no. cmd.ray output is fetched as pixels from the backend instead
(Mode P), never reproduced client-side.
Where it will definitively NOT match
-
Ray tracing (
cmd.ray) — irreproducible in three.js.RayRender(packages/engine/layer1/Ray.cpp, 7827 lines) is a CPU ray tracer with real shadows, multi-light specular, interior colors, andray_trace_modecel-shading/outlines (packages/engine/layer1/Ray.cpp:5570-5607, 6472-6474).ray_trace_modeexists only in the ray tracer — grep ofpackages/engine/layer1/SceneRay.cppandpackages/engine/layer1/SceneRender.cppfound zero references to it in the GL path. There is no path to parity here. The web client must callcmd.ray()+cmd.png()server-side and display the resulting bitmap. This is a mode switch, not a rendering feature — and the UX must make that explicit. -
Order-independent transparency (
transparency_mode 3). PyMOL binds an OIT accumulation framebuffer with multiple draw buffers and a dedicatedoitshader (packages/engine/layer1/SceneRender.cpp:939-995,packages/engine/data/shaders/oit.vs/.fs,packages/engine/layer0/PostProcess.cpp:60, 86,TM3_IS_ONEBUFhandling atpackages/engine/layer1/SceneRender.cpp:979, 1101, 1466). WebGL2 has noGL_EXT_draw_buffers-style MRT parity issues but does lack per-sample control PyMOL relies on; an approximate WBOIT is achievable but will not be pixel-identical. -
CPU depth-sorted alpha triangles (
transparency_mode != 3). The other transparency path sorts triangles on the CPU per frame:CGOOptimizeToVBOIndexedWithColorEmbedTransparentInfoembedsz_value,ix, andsort_memarrays into the CGO (packages/engine/layer1/CGO.cpp:3395-3405), andRepSurface::renderallocatest_buf/z_value/ixsizedNTand re-sorts every frame (packages/engine/layer2/RepSurface.cpp:1969-1984). Reproducing this in JS means per-frame triangle sorting of up to 10^6 triangles in the browser — that will not hold 60 fps. Either accept sort artifacts or ship the sorted index buffer from Python each frame (which reintroduces per-frame cost). -
Stereo. PyMOL implements 9 stereo modes: quadbuffer, crosseye, walleye, geowall, sidebyside,
stencil-by-row/column/checkerboard/custom, anaglyph, openvr
(
packages/engine/layer1/SceneRay.cpp:154-175, 515-713). Quad-buffer stereo and stencil-parity stereo are impossible in a browser. Anaglyph and side-by-side are reproducible. OpenVR is out. -
Post-process antialiasing.
antialias_shaderselects FXAA (1 stage) or SMAA (3 stages) (packages/engine/layer1/SceneRender.cpp:613-627, 1791). Reproducible with effort, but PyMOL’s exact SMAA parameters must be ported or edges will differ visibly at high zoom. -
Labels.
cRepLabelis texture-atlas quads driven byCGO_DRAW_LABEL/CGO_DRAW_LABELS(packages/engine/layer1/CGO.h:224-228) and a texture-size heuristic (InvalidateShaderCGOIfTextureNeedsUpdate,packages/engine/layer2/RepLabel.cpp:117,MAX_LABEL_TEXTURE_SIZE 256,:114). Glyphs are CPU-reachable (packages/engine/layer1/Character.cpp:122) but the atlas must be rebuilt client-side. Font rasterization will differ from PyMOL’s FreeType output at the pixel level. Expect near-miss, not match. -
cRepCallback(14). Objects created bycmd.load_callbackexecute arbitrary Python that issues raw OpenGL.packages/engine/layer2/ObjectCallback.cppexists for exactly this. There is no geometry to extract — by construction. Any plugin using it is unsupported in the web client. -
cRepVolume(20) andcRepSlice(16). 3D-texture ray marching (packages/engine/data/shaders/volume.fs, 47 lines). Not mesh geometry. Requires a separate 3D-texture upload path (cmd.get_volume_fieldexists,packages/engine/layer4/Cmd.cpp:6510,packages/engine/layer4/Cmd.cpp:727→FieldAsNumPyArray) and a hand-ported volume shader. Treat as a separate epic. -
Ambient occlusion.
VAOper-vertex occlusion is computed on the surface (packages/engine/layer2/RepSurface.cpp:2780-2800) and shipped as a vertex attribute — that part transfers fine. Butambient_occlusion_modealso has a smoothing pass with its own map (ambient_occlusion_map,packages/engine/layer2/RepSurface.cpp:2368) whose result is baked intoVAO, so as long as the client readsVAOafter rebuild, this one actually matches. -
Grid mode.
GridMode::ByObject / ByObjectStates / ByObjectByState(packages/engine/layer1/SceneRay.cpp:179-211, 306-320) splits the viewport into N sub-viewports with adjusted aspect ratios. Reproducible with N three.js viewports, but the layout math (GridUpdate,grid.asp_adjust) must be ported exactly or panels will misalign. -
Precision. All PyMOL geometry is
float32and all colors arefloat32RGB in [0,1] (CLIP_COLOR_VALUE/CONVERT_COLOR_VALUE,packages/engine/layer1/CGO.h:54-55). Optional byte-packed normals/colors are gated oncgo_shader_ub_color/cgo_shader_ub_normal(packages/engine/layer1/CGO.cpp:2939-2961,CLIP_NORMAL_VALUEatpackages/engine/layer1/CGO.h:58-61). If the accessor emits float32 and the client uses float32, this is exact. -
Picking. PyMOL picks by rendering a pick-color buffer (
CGORenderPicking,packages/engine/layer2/RepSphere.cpp:120,packages/engine/layer1/ScenePicking.cpp). TheCGO_PICK_COLORpayload (atom index, bond index,packages/engine/layer1/CGO.h:141-142) is in the extracted arrays, so client-side GPU picking is achievable — but the encoding (Picking.h,cPickableAtom = -1…cPickableThrough = -5,packages/engine/modules/pymol/cgo.py:73-77) must be replicated bit-for-bit or selections will land on the wrong atoms.
9. Questions this map left open, and how they were closed
- No Python hook fires on Rep rebuild/invalidation. Confirmed by grep over
packages/engine/layer1/P.cpp,packages/engine/layer4/Cmd.cppandpackages/engine/modules/pymol/*.py. None was added; §6f explains what replaced it. - Is
RepSurface::ATpopulated in all surface modes, or only withpick_surface?ATis declared atpackages/engine/layer2/RepSurface.cpp:83; not every write site was traced here.docs/spikes/picking.mdanswered the practical question instead — surface picking runs on the backend against a real GL pick pass (15/15 clicks resolved), so client-side reliance onATnever became load-bearing. - The byte layout of the pick-color sub-block inside
CGO_DRAW_ARRAYS(VERTEX_PICKCOLOR_RGBA_SIZE = 1float holding 4 packed bytes, plus 2 index floats,packages/engine/layer1/CGO.cpp:60-63). The offset arithmetic atpackages/engine/layer1/CGO.cpp:1665(pickColorVals = nxtVals + VERTEX_PICKCOLOR_RGBA_SIZE * nverts) implies the RGBA sub-array precedes the index sub-array in the same block. Verified at runtime bydocs/spikes/geometry-accessor.md, which is whatpackages/bridge/tenmol_bridge/render/modeg.py::_deinterleave_i32depends on. _PYMOL_NO_RAYguardsSceneRayentirely (packages/engine/layer1/SceneRay.cpp:94-96). Nosetup.pyflag sets it, so ray export is compiled in for this fork’s builds.- Whether
pymol._cachesurface entries survivecmd.delete/rebuild cycles cleanly enough to be a reliable feed —_cache_purge(packages/engine/modules/pymol/internal.py:49-78) evicts by size and access time. Moot: §6’s accessor readsRep[]directly rather than the cache.