Skip to content

MLD304: Renderer-divergent effect reached without a guard in a multi-renderer run

qual reports MLD304 only when the run actually targets more than one renderer (--profile all with both a Cairo and an OpenGL profile) and the analyzer proves that a renderer-divergent effect is reached on every path.

  • Default severity: warning
  • Minimum confidence: medium
  • Implementation phase: 4
  • Fix: none

Two divergence families are implemented.

Fixed-object removal (DESIGN 3.5)

ThreeDScene.remove_fixed_in_frame_mobjects / remove_fixed_orientation_mobjects only unregister the camera fix under Cairo, but the OpenGL branch additionally removes the object from the Scene. Rendering the same scene with both renderers therefore produces different membership โ€” the object stays visible under Cairo and vanishes under OpenGL. The diagnostic's evidence names the affected registry (fixed_in_frame / fixed_orientation) and points back at the certain registration site of the removed object.

Wrong (with --profile all over cairo + opengl profiles):

self.add_fixed_in_frame_mobjects(label)
self.remove_fixed_in_frame_mobjects(label)   # visible on cairo, gone on opengl
self.wait()

Right:

self.add_fixed_in_frame_mobjects(label)
self.remove_fixed_in_frame_mobjects(label)
self.remove(label)                           # pin the membership explicitly

MovingCameraScene camera-frame semantics

Under Cairo, self.camera on a MovingCameraScene is a MovingCamera whose frame is an animatable mobject (and auto_zoom exists). The OpenGL renderer ignores camera_class and installs an OpenGLCamera without that contract (scene.py Scene.__init__, moving_camera_scene.py), so an unguarded self.camera.frame / self.camera.auto_zoom path diverges between the targeted renderers. The rule fires only when the scene's camera contract is proven MovingCamera (fully resolved bases) and the access sits on the straight-line prefix of construct โ€” reached on every render.

Wrong (with --profile all over cairo + opengl profiles):

class Tour(MovingCameraScene):
    def construct(self):
        square = Square()
        self.add(square)
        self.play(self.camera.frame.animate.move_to(square))  # cairo-only contract

Right:

class Tour(MovingCameraScene):
    def construct(self):
        square = Square()
        self.add(square)
        if config.renderer == "cairo":          # guard the cairo-only path
            self.play(self.camera.frame.animate.move_to(square))

(or restrict the scene to a Cairo-only profile so the run is single-renderer.)

Silent by design

  • single-renderer runs โ€” a project that explicitly targets one renderer is never asked for renderer guards (DESIGN 7.4 prose);
  • any fixed-removal call behind a branch: the event certainty joins to Maybe, so a renderer guard (or any other condition) silences the rule;
  • camera-frame uses behind a branch, loop, try, with, early return, lambda, comprehension, conditional-expression branch, or in a helper method: not an all-paths use;
  • a MovingCameraScene subclass that never touches the camera frame: subclassing alone is not a divergence certainty;
  • mixed or unresolved base chains: the camera kind stays Unknown and is never guessed.