Graphics objects (gobj)

This cluster is the device-independent representation of everything that gets drawn: geometry (Geom, GeomPrimitive), the vertex-data layer (GeomVertexData / GeomVertexFormat / GeomVertexArrayData), textures (Texture, TextureStage, SamplerState), shaders (Shader, ShaderBuffer), materials, lenses, and the GPU-resource bookkeeping (PreparedGraphicsObjects and the per-GSG *Context handles). Everything here lives first in system RAM as renderer-agnostic data; it is converted to a backend-specific layout by GeomMunger and uploaded to the GPU lazily through the abstract GraphicsStateGuardianBase (gsgbase) interface on first render. The two directories are deliberately separate Panda packages: gobj (libp3gobj) holds the concrete data classes; gsgbase (libp3gsgbase) holds only the abstract GSG/output interfaces, kept in their own package “so we can avoid circular build dependency problems” (graphicsStateGuardianBase.h). Almost every class in gobj is built around the copy-on-write + pipeline-cycler concurrency pattern so a render thread can read geometry lock-free while an app thread mutates it.

gobj

What it is. panda/src/gobj is the largest leaf package in Panda’s graphics stack. It defines the in-memory data model for renderable content: a Geom bundles one GeomVertexData (the vertex table) with one or more GeomPrimitives (index lists describing how vertices form triangles/lines/points). Separately it defines textures, shaders, materials, lenses, the format/registry machinery, the munger that bridges to a specific renderer, and the LRU/paging systems that keep both vertex data and GPU resources within a memory budget. It has no knowledge of OpenGL/DirectX/Vulkan — those backends (panda/src/glstuff, dxgsg9, etc.) consume these classes through the gsgbase double-dispatch interface.

The geometry core: Geom, GeomPrimitive, GeomVertexData

  • Geom (geom.h / geom.cxx, ~61 KB of impl) — class Geom : public CopyOnWriteObject, public GeomEnums. A container associating one COWPT(GeomVertexData) with a pvector<COWPT(GeomPrimitive)>. All primitives in a Geom share the same vertex table and the same render state. Key methods: set_vertex_data/modify_vertex_data, add_primitive, draw() (the public entry into rendering, takes a GraphicsStateGuardianBase *), prepare_now(), and a family of in-place transforms (decompose_in_place, unify_in_place, make_points_in_place, doubleside_in_place). It maintains a per-format munge cache (Geom::CacheEntry, keyed by (GeomVertexData*, GeomMunger*)) and a per-GSG context map (Contexts _contexts).

  • GeomPrimitive (geomPrimitive.h / .cxx, ~75 KB) — class GeomPrimitive : public CopyOnWriteObject, public GeomEnums. Abstract base for an ordered list of vertex indices. The index list itself is stored as a GeomVertexArrayData (so index buffers reuse the same paging/upload machinery as vertex buffers). Concrete subclasses (all : public GeomPrimitive): GeomTriangles, GeomTristrips, GeomTrifans, GeomPatches, GeomLines, GeomLinestrips, GeomPoints, plus adjacency variants. make_copy(), get_primitive_type(), and draw() are pure virtual; each subclass dispatches to the matching GraphicsStateGuardianBase::draw_triangles/draw_tristrips/.... Notable: indices can be non-indexed (a first_vertex/num_vertices range) or indexed (uint8/uint16/uint32, auto-elevated via consider_elevate_index_type); composite types (strips/fans) use a _ends array; _mins/_maxs cache the index range per sub-primitive.

  • GeomVertexData (geomVertexData.h / .cxx, ~90 KB) — class GeomVertexData : public CopyOnWriteObject, public GeomEnums. The vertex table: a list of GeomVertexArrayData arrays interpreted through a GeomVertexFormat. Holds optional animation tables (TransformTable, TransformBlendTable, SliderTable) and a cached _animated_vertices result. Key methods: convert_to(new_format), animate_vertices(force, thread) (runs CPU skinning/morphing), transform_vertices(mat), scale_color/set_color, set_num_rows. App code should not poke arrays directly — it goes through reader/writer cursors (below).

The vertex-data subsystem (format, arrays, cursors, animation, paging)

  • GeomVertexFormat (geomVertexFormat.h / .cxx) — final : public TypedWritableReferenceCount, public GeomEnums. Describes the physical layout: a list of GeomVertexArrayFormats, each a list of GeomVertexColumns (name + numeric type + components). Formats must be registered before use (GeomVertexFormat::register_format); registration deduplicates through a global Registry guarded by LightReMutex _lock, so two logically equal formats return the same pointer and can be compared by identity. Provides standard prebuilt formats (get_v3n3t2, get_v3c4, etc.) and the all-important get_post_animated_format() / get_union_format() used by the munger.

  • GeomVertexArrayFormat (geomVertexArrayFormat.h) and GeomVertexColumn (geomVertexColumn.h, ~100 KB of .cxx — it contains every numeric-type read/write packer) define one array’s columns. GeomVertexColumn : public GeomEnums.

  • GeomVertexArrayData (geomVertexArrayData.h / .cxx) — class GeomVertexArrayData : public CopyOnWriteObject, public SimpleLruPage, public GeomEnums. One contiguous byte buffer (“stream”/vertex buffer in DX/GL terms). The raw bytes live in a VertexDataBuffer, which can be paged out (see below). Inheriting SimpleLruPage is what enrolls each array in the global vertex-memory LRU. Access goes through GeomVertexArrayDataHandle.

  • Cursors: GeomVertexReader / GeomVertexWriter (both : public GeomEnums) and GeomVertexRewriter : public GeomVertexWriter, public GeomVertexReader. These are the high-level, type-safe way to read/write columns by InternalName. The .I files are enormous (geomVertexWriter.I ~37 KB) because they inline a specialized path per numeric type.

  • Animation: GeomVertexAnimationSpec records whether a format animates on CPU (AT_panda) or GPU (AT_hardware). The transform pipeline is VertexTransform (abstract; get_matrix/accumulate_matrix) → TransformBlend (a weighted set of transforms for one vertex) → TransformBlendTable → referenced from GeomVertexData. VertexSlider/SliderTable drive morph targets. UserVertexTransform/UserVertexSlider are app-settable leaves.

  • Paging (vertex data to disk): VertexDataBuffer (vertexDataBuffer.h) holds the bytes; VertexDataPage : public SimpleAllocator, public SimpleLruPage (vertexDataPage.h) packs many buffers onto a page that can transition RC_resident RC_compressed RC_disk under memory pressure; VertexDataBook is the allocator-of-pages; VertexDataSaveFile is the on-disk backing store. Background threads (config vertex-data-page-threads) do the compress/spill work.

The texture subsystem

  • Texture (texture.h, 50 KB header; texture.cxx, 339 KB — the single biggest file in the cluster) — class Texture : public TypedWritableReferenceCount, public Namable. Holds a CPU RAM image plus all the metadata (TextureType 1d/2d/3d/array/cube/buffer, ComponentType, Format — dozens of formats incl. sRGB, integer, packed-float, depth). It is pipelined (CData/PipelineCycler) and tracks a per-GSG TextureContext. Gotcha (community-confirmed): Panda frees the system-RAM image once the texture is uploaded to the GPU; if the GPU copy is later lost (e.g. cube map mipmaps), reload can fail — set Texture.keep_ram_image = True or rely on keep-texture-ram. See GitHub issue #1091 “Loading/unloading cubemap texture with manual mipmaps needs TexturePool.release()”, https://github.com/panda3d/panda3d/issues/1091 (“Panda unloads the texture from RAM once it’s been uploaded to the GPU… Texture.keep_ram_image = True works and solves the issue”).

  • TextureStage (textureStage.h) — names a slot in the fixed-function multitexture pipeline and its combine mode; pooled via TextureStagePool.

  • SamplerState (samplerState.h) — class SamplerState : public MemoryBase. A value object (not ref-counted) describing wrap modes (WM_clamp/repeat/mirror/...), min/mag FilterType, anisotropy, LOD bias, border color. Decoupled from Texture since rdb’s 2014 rework so the same image can be sampled differently in different places; the GSG prepares a SamplerContext for it.

  • TexturePool (texturePool.h, ~45 KB impl) — process-global registry/cache keyed by filename, with a filter chain (TexturePoolFilter, PythonTexturePoolFilter). VideoTexture : public Texture, public AnimInterface is the base for movie textures. TexturePeeker reads pixels back from a RAM image; TextureCollection is a list type. TextureReloadRequest is the async background reload task (threads: texture-reload-num-threads).

Shaders and materials

  • Shader (shader.h; shader.cxx ~120 KB) — class Shader : public TypedWritableReferenceCount. Holds source/metadata for SL_Cg / SL_GLSL / SL_HLSL / SL_SPIR_V, split by ShaderType (vertex/fragment/geometry/tess/compute). Shader::load/make/make_compute are the factories. The big ShaderMatInput enum (SMO_model_to_view, SMO_attr_colorscale, …) enumerates the auto-bound shader inputs Panda can compute from render state. prepare() returns a PT(AsyncFuture) (compilation can be async). ShaderBuffer : public TypedWritableReferenceCount, public Namable, public GeomEnums (shaderBuffer.h) is GPU storage for compute (SSBO).

  • Material (material.h) — class Material : public TypedWritableReferenceCount, public Namable. Lighting properties; supports both classic (ambient/diffuse/specular) and metalness (base color + metallic + roughness) PBR workflows. MaterialPool deduplicates.

  • Lens (lens.h; lens.cxx ~64 KB) — class Lens : public TypedWritableReferenceCount. Camera/projection abstraction with extrude/project. Subclasses: PerspectiveLens, OrthographicLens, MatrixLens. Lives in gobj (not display) because a Spotlight also uses a Lens. ParamTextureSampler/ParamTextureImage (paramTexture.h, both : public ParamValueBase) wrap a texture+sampler (or image) as a shader-input parameter.

Munging, caching, and GPU preparation

  • GeomMunger (geomMunger.h / .cxx) — class GeomMunger : public TypedReferenceCount, public GeomEnums. The pivotal bridge: converts a GeomVertexData from its authoring format into one the backend can consume, and applies render-state-driven vertex changes (e.g. baking a ColorScaleAttrib into vertex colors). Each backend subclasses it (CLP(GeomMunger), DXGeomMunger). Registered mungers are deduplicated like formats (same operation ⇒ same pointer). The GSG hands one out via GraphicsStateGuardianBase::get_geom_munger(state, thread). Performance note (trusted forum): “‘munge’ is the time required to massage the GVD into a format suitable for the backend, and is what fills up the geom-vertex-cache” — https://discourse.panda3d.org/t/8211. Matching your authoring format to the GSG’s preferred format avoids munge cost.

  • GeomCacheManager / GeomCacheEntry (geomCacheManager.h, geomCacheEntry.h) — a global LRU bounding the total munge/decompose cache (geom-cache-size, geom-cache-min-frames). AnimateVerticesRequest runs CPU vertex animation off-thread.

  • PreparedGraphicsObjects (preparedGraphicsObjects.h; .cxx ~54 KB) — class PreparedGraphicsObjects : public ReferenceCount. The per-GSG (or shared-context) registry of GPU resources. For each resource kind (texture, sampler, geom, shader, vertex buffer, index buffer, shader buffer) it offers enqueue_* / dequeue_* / is_*_prepared / release_* / prepare_*_now. It owns the *Context objects and enforces the graphics-memory budget through BufferResidencyTracker + AdaptiveLru. Protected by a ReMutex.

  • Context objects (per-GSG opaque handles): SavedContext : public TypedObjectBufferContext : public SavedContext, private LinkedListNodeTextureContext : public BufferContext, public AdaptiveLruPage; also VertexBufferContext, IndexBufferContext, GeomContext, ShaderContext, SamplerContext, BufferContextChain. Backends subclass these to stash the real API handle (e.g. a GL texture id).

  • Memory machinery: SimpleAllocator (block sub-allocation), SimpleLru + SimpleLruPage (basic LRU; vertex arrays/pages are pages), AdaptiveLru + AdaptiveLruPage (frequency-weighted LRU used for GPU residency; textures are pages). Config graphics-memory-limit, adaptive-lru-weight.

Shared enums and naming

  • GeomEnums (geomEnums.h) — pure scoping base providing UsageHint (UH_client/stream/dynamic/static/unspecified), GeomRendering (capability bitmask — the diff between a Geom’s required bits and the GSG’s get_supported_geom_rendering() tells the munger what to fix up), ShadeModel, PrimitiveType, NumericType, Contents (semantic: C_point, C_normal, C_color, C_texcoord, C_morph_delta, …), and AnimationType. Inherited by virtually every class here.

  • InternalName (internalName.h / .cxx) — final : public TypedWritableReferenceCount. Interned, hierarchical (‘.’-separated) immutable name tokens used for vertex columns, texture stages, and shader inputs. Interning gives O(1) pointer comparison; predefined singletons (get_vertex(), get_normal(), get_color(), get_texcoord(), get_transform_blend(), …) name the standard columns.

How it plugs into the rest of the engine. Up the stack, GeomNode (panda/src/pgraph) holds Geoms in the scene graph; the cull traversal collects them into CullableObjects, and CullBin/GraphicsEngine eventually call Geom::draw(). Geom::draw() and the *PipelineReader classes call down into a concrete GraphicsStateGuardian (panda/src/display + a backend like glgsg) exclusively through the abstract GraphicsStateGuardianBase vtable. Textures/shaders/materials are referenced by RenderAttribs (TextureAttrib, ShaderAttrib, MaterialAttrib) in pgraph but the objects themselves live here. Bam serialization (panda/src/putil) reads/writes these via the write_datagram/fillin/make_from_bam hooks present on most classes.

Where to start reading (entry points).

  • Adding/altering a vertex feature → geomVertexFormat.cxx (registration, standard formats, get_post_animated_format) and geomVertexData.cxx (convert_to, animate_vertices).

  • A primitive/index bug → geomPrimitive.cxx (do_make_indexed, consider_elevate_index_type, decompose_impl) and the relevant geomTriangles.cxx / geomTristrips.cxx.

  • A “wrong colors / unexpected reformatting” bug → geomMunger.cxx (munge_format_impl, munge_data_impl) and the backend’s munger subclass.

  • A texture upload/format/memory bug → texture.cxx (do_reload_ram_image, consider_auto_process_ram_image, format tables) and preparedGraphicsObjects.cxx.

  • GPU-resource lifecycle/leak → preparedGraphicsObjects.cxx (enqueue_*/release_*/prepare_*_now).

Gotchas / design rationale / config.

  • COW + pipeline: Each core class has a private CData : public CycleData plus a PipelineCycler, and exposes *PipelineReader/*PipelineWriter helpers. The render thread reads a frozen cycle stage while the app thread writes a copy — never bypass this by holding raw pointers across frames.

  • Format/munger/pool identity: Because registered formats, mungers, and pooled objects deduplicate to a single pointer, code routinely compares them by pointer; if you build a format and forget to register_format it, identity comparisons silently break.

  • Direct buffer writes: You can write raw vertex bytes via GeomVertexArrayDataHandle::set_data / get_write_pointer (trusted forum thread https://discourse.panda3d.org/t/3646), but you must set_num_rows first and keep the format in sync.

  • Texture RAM eviction (issue #1091, above) is the most common texture footgun.

  • Selected config vars (all in config_gobj.cxx): vertex-buffers, vertex-arrays, hardware-animated-vertices, matrix-palette, vertices-float64, vertex-column-alignment, vertex-animation-align-16, vertex-colors-prefer-packed, geom-cache-size, geom-cache-min-frames, released-vbuffer-cache-size, released-ibuffer-cache-size, graphics-memory-limit, sampler-object-limit, adaptive-lru-weight, vertex-data-small-size, vertex-data-page-threads, vertex-save-file-directory, keep-texture-ram, driver-compress-textures, driver-generate-mipmaps, textures-power-2, textures-square, texture-reload-num-threads, max-texture-dimension, dump-generated-shaders, cache-generated-shaders, glsl-preprocess, default-near/default-far/default-fov, lens-geom-segments.

gsgbase

What it is. panda/src/gsgbase is a tiny package (two real classes) that exists purely to break a build-dependency cycle. The renderable data in gobj needs to call into a GSG to draw, but a concrete GSG (in display/backends) depends on gobj. The fix is to put the abstract GSG interface here, in a package that gobj can depend on, so Geom/GeomPrimitive can invoke gsg->draw_triangles(...) without a circular link. As the header states: “It lives in a separate class in its own package so we can avoid circular build dependency problems” (graphicsStateGuardianBase.h).

Key classes.

  • GraphicsStateGuardianBase (graphicsStateGuardianBase.h / .cxx) — class GraphicsStateGuardianBase : public TypedWritableReferenceCount. A pure-virtual “device driver” interface enumerating everything a renderer must implement: capability queries (get_max_texture_dimension, get_supported_geom_rendering, prefers_triangle_strips, get_supports_compressed_texture_format, get_supports_hlsl), resource preparation (prepare_texture/prepare_geom/prepare_shader/prepare_vertex_buffer/prepare_index_buffer/prepare_sampler/prepare_shader_buffer and their release_*), state binding (set_state_and_transform, get_geom_munger, bind_light), and the double-dispatch draw protocol: begin_draw_primitives → one of draw_triangles / draw_triangles_adj / draw_tristrips / draw_tristrips_adj / draw_trifans / draw_patches / draw_lines / draw_lines_adj / draw_linestrips / draw_linestrips_adj / draw_pointsend_draw_primitives. Each GeomPrimitive subclass picks the matching method, so the primitive type is resolved without RTTI or switch statements. It also keeps the global GSG registry (add_gsg/get_gsg/get_default_gsg) used so a Texture/Geom can be released from all GSGs.

  • GraphicsOutputBase (graphicsOutputBase.h / .cxx) — class GraphicsOutputBase : public TypedWritableReferenceCount. A two-method abstract base (set_sort, get_texture) for GraphicsOutput (windows/buffers), again split out so lower packages can refer to an output without depending on display.

How it plugs in. Concrete subclass GraphicsStateGuardian (panda/src/display) fleshes most of these out; backend GSGs (GLGraphicsStateGuardian, DXGraphicsStateGuardian9, Vulkan) subclass that. gobj (and pgraph, via cull) only ever sees the base type — Geom::draw(GraphicsStateGuardianBase *gsg, ...). The forward-declaration block at the top of graphicsStateGuardianBase.h (every Geom*, Texture, attrib, light type) documents exactly which engine objects participate in the GSG dispatch.

Where to start. To add a capability flag or a new draw entry point, edit graphicsStateGuardianBase.h (add the pure-virtual), then implement it in panda/src/display/graphicsStateGuardian.cxx and each backend. config_gsgbase.cxx only does init_type() registration — there are no gsgbase config vars. Community pointer on profiling: time attributed to begin_draw_primitives in PStats is the GSG draw-setup cost (Discord, https://discord.com/channels/524691714909274162/533306680406966273/1126767468858331156).

Where to start (this cluster)

A new contributor should orient through these files, in order:

  1. panda/src/gobj/geom.h — the top-level container; from Geom::draw() you can trace the whole render path.

  2. panda/src/gobj/geomEnums.h — small, and decodes the vocabulary (UsageHint, GeomRendering, NumericType, Contents) used everywhere else.

  3. panda/src/gobj/geomVertexFormat.h + geomVertexData.h — how vertices are described and stored; read alongside geomVertexReader.h/geomVertexWriter.h for the cursor API.

  4. panda/src/gobj/geomPrimitive.h — index/primitive model and the draw_* dispatch tie-in.

  5. panda/src/gobj/geomMunger.h — the format-conversion bridge; the single most important class for understanding the gobj↔backend boundary.

  6. panda/src/gsgbase/graphicsStateGuardianBase.h — the abstract driver interface every backend implements; the forward-declare block is a map of who talks to the GSG.

  7. panda/src/gobj/preparedGraphicsObjects.h — GPU-resource lifecycle and the memory budget.

  8. panda/src/gobj/texture.h and shader.h — the other two big resource types; large, so start from the published factory methods (Texture::read, Shader::load/make).