Offscreen buffer rendering issues on MacOS Apple Silicon

I’m facing issue with a rendering offscreen buffer on Github Action runners with macos-14 and macOS-15 labels, i.e. MacOS 14/15 on Apple Silicon M2 (ARM). In constrast, everything is working fine on macOS-13, i.e. MacOS 13 on Intel x86.

I guess it has something to do with the integrated GPU that is different between Intel x86 and Apple Silicon, but I’m not really able to understand what is the issue and how it could be fixed…

I’m using the following snippet to generate “screenshots”:

texture = buffer.get_texture()
buffer.set_one_shot(True)
self.graphics_engine.render_frame()
image = texture.get_ram_image_as('RGB')
xsize, ysize = texture.get_x_size(), texture.get_y_size()
rgb_array = np.frombuffer(image, np.uint8).reshape((ysize, xsize, -1))

Here are the screenshots that I get on Github Action runners and my own laptop respectively:

Any idea what may be happening here ? There is no task manager running in the background. Everything is happening in the main thread without multi-threading since I only render the scene when needed. I could try to right a minimum reproducible script, but it would be quite time consuming because it only happens on Github Action. I have no issue on my own MacBook using Apple Silicon M3. Could it is related to MacOS runners not being capable of running MPS due to hardware limitations ?

For the record, here is how the sky box is generated (I could also provide make_gradient_skybox method if needed). The implementation is based on this other thread:

self.skybox = make_gradient_skybox(
    SKY_TOP_COLOR, SKY_BOTTOM_COLOR, 0.35, 0.17)
self.skybox.set_shader_auto()
self.skybox.set_light_off()
self.skybox.hide(self.LightMask | self.UserDepthCameraMask)
pivot = self.render.attach_new_node("pivot")
effect = CompassEffect.make(self.camera, CompassEffect.P_pos)
pivot.set_effect(effect)
self.skybox.reparent_to(pivot)
effect = BillboardEffect.make(
    Vec3.up(), False, True, 0.0, NodePath(),
    Point3(0.0, -10.0, 0.0), False)
self.skybox.set_effect(effect)

I have an M2 Macbook; is there something I could run to see if it happens for me?

On the runners, what kind of implementation of OpenGL do you get? You can get some information on that using notify-level-glgsg debug.

You can get some information on that using notify-level-glgsg debug .

Good point ! I just started a job this option enabled.

is there something I could run to see if it happens for me?

Yes, there is ! First, you need to install jiminy-py via pip (pip install --upgrade jiminy-py). Here is a self-contained script that is generated the same screenshot than the CI job:

import os
import tempfile
import xml.etree.ElementTree as ET

import numpy as np
import matplotlib.pyplot as plt

import jiminy_py.core as jiminy
from jiminy_py.simulator import Simulator

# Model parameters
MASS_SEGMENTS = 0.1
INERTIA_SEGMENTS = 0.001
LENGTH_SEGMENTS = 0.01
N_FLEXIBILITY = 40

# Create temporary urdf file
fd, urdf_path = tempfile.mkstemp(
    prefix="flexible_arm_", suffix=".urdf")
os.close(fd)

# Procedural model generation
robot = ET.Element("robot", name="flexible_arm")
ET.SubElement(robot, "link", name="base")
for i in range(N_FLEXIBILITY + 1):
    link = ET.SubElement(robot, "link", name=f"link{i}")
    visual = ET.SubElement(link, "visual")
    ET.SubElement(
        visual, "origin", xyz=f"{LENGTH_SEGMENTS/2} 0 0", rpy="0 0 0")
    geometry = ET.SubElement(visual, "geometry")
    ET.SubElement(geometry, "box", size=f"{LENGTH_SEGMENTS} 0.025 0.01")
    material = ET.SubElement(visual, "material", name="")
    ET.SubElement(material, "color", rgba="0 0 0 1")
    inertial = ET.SubElement(link, "inertial")
    ET.SubElement(
        inertial, "origin", xyz=f"{LENGTH_SEGMENTS/2} 0 0", rpy="0 0 0")
    ET.SubElement(inertial, "mass", value=f"{MASS_SEGMENTS}")
    ET.SubElement(
        inertial, "inertia", ixx="0", ixy="0", ixz="0", iyy="0", iyz="0",
        izz=f"{INERTIA_SEGMENTS}")
motor = ET.SubElement(
    robot, "joint", name="base_to_link0", type="revolute")
ET.SubElement(motor, "parent", link="base")
ET.SubElement(motor, "child", link="link0")
ET.SubElement(motor, "origin", xyz="0 0 0", rpy=f"{np.pi/2} 0 0")
ET.SubElement(motor, "axis", xyz="0 0 1")
ET.SubElement(
    motor, "limit", effort="100.0", lower=f"{-np.pi}", upper=f"{np.pi}",
    velocity="10.0")
for i in range(1, N_FLEXIBILITY + 1):
    joint = ET.SubElement(
        robot, "joint", name=f"link{i-1}_to_link{i}", type="fixed")
    ET.SubElement(joint, "parent", link=f"link{i-1}")
    ET.SubElement(joint, "child", link=f"link{i}")
    ET.SubElement(
        joint, "origin", xyz=f"{LENGTH_SEGMENTS} 0 0", rpy="0 0 0")
tree = ET.ElementTree(robot)
tree.write(urdf_path)

# Create and initialize the robot
robot = jiminy.Robot()
robot.initialize(urdf_path, False)

# Add motors to the robot
for joint_name in ("base_to_link0",):
    motor = jiminy.SimpleMotor(joint_name)
    robot.attach_motor(motor)
    motor.initialize(joint_name)

# Configure the robot
robot_options = robot.get_options()
motor_options = robot_options["motors"]
for motor in robot.motors:
    motor_options[motor.name]["enableVelocityLimit"] = False
    motor_options[motor.name]['enableEffortLimit'] = False
    motor_options[motor.name]['enableArmature'] = False
robot.set_options(robot_options)

# Remove temporary file
os.remove(urdf_path)

# Create a simulator using this robot and controller
simulator = Simulator(
    robot,
    viewer_kwargs=dict(
        camera_pose=((0.0, -2.0, 0.0), (np.pi/2, 0.0, 0.0), None)
    ))

# Set initial condition and simulation duration
q0, v0 = np.array([0.]), np.array([0.])
t_end = 4.0

# Run a first simulation without flexibility
simulator.simulate(
    t_end, q0, v0, is_state_theoretical=True, show_progress_bar=False)

# Extract the final configuration
q_rigid = simulator.robot_state.q.copy()

# Render the scene
img_rigid = simulator.render(return_rgb_array=True)

# Check different flexibility ordering
for order in (range(N_FLEXIBILITY), range(N_FLEXIBILITY)[::-1]):
    # Specify joint flexibility parameters
    model_options = simulator.robot.get_model_options()
    model_options['dynamics']['enableFlexibility'] = True
    model_options['dynamics']['flexibilityConfig'] = [{
        'frameName': f"link{i}_to_link{i+1}",
        'stiffness': np.zeros(3),
        'damping': np.zeros(3),
        'inertia': np.full(3, fill_value=1e6)
    } for i in order]
    simulator.robot.set_model_options(model_options)

    # Launch the simulation
    simulator.simulate(
        t_end, q0, v0, is_state_theoretical=True,
        show_progress_bar=False)

    # Render the scene
    plt.figure()
    plt.imshow(simulator.render(return_rgb_array=True))
plt.show()

Here is the trace:

:display:gsg:glgsg(debug): GL_VENDOR = Apple Inc.
:display:gsg:glgsg(debug): GL_RENDERER = Apple Software Renderer
:display:gsg:glgsg(debug): GL_VERSION = 2.1 APPLE-21.0.27
:display:gsg:glgsg(debug): GL_VERSION decoded to: 2.1
:display:gsg:glgsg(debug): GL_SHADING_LANGUAGE_VERSION = 1.20
:display:gsg:glgsg(debug): Detected GLSL version: 1.20
:display:gsg:glgsg(debug): Using compatibility profile
:display:gsg:glgsg(debug): GL Extensions:
  GL_APPLE_aux_depth_stencil             GL_APPLE_client_storage
  GL_APPLE_element_array                 GL_APPLE_fence
  GL_APPLE_float_pixels                  GL_APPLE_flush_buffer_range
  GL_APPLE_flush_render                  GL_APPLE_packed_pixels
  GL_APPLE_pixel_buffer                  GL_APPLE_rgb_422
  GL_APPLE_row_bytes                     GL_APPLE_specular_vector
  GL_APPLE_texture_range                 GL_APPLE_transform_hint
  GL_APPLE_vertex_array_object           GL_APPLE_vertex_array_range
  GL_APPLE_vertex_point_size             GL_APPLE_vertex_program_evaluators
  GL_APPLE_ycbcr_422                     GL_ARB_color_buffer_float
  GL_ARB_depth_buffer_float              GL_ARB_depth_clamp
  GL_ARB_depth_texture                   GL_ARB_draw_buffers
  GL_ARB_draw_elements_base_vertex       GL_ARB_draw_instanced
  GL_ARB_fragment_program                GL_ARB_fragment_program_shadow
  GL_ARB_fragment_shader                 GL_ARB_framebuffer_object
  GL_ARB_framebuffer_sRGB                GL_ARB_half_float_pixel
  GL_ARB_half_float_vertex               GL_ARB_imaging
  GL_ARB_instanced_arrays                GL_ARB_multisample
  GL_ARB_multitexture                    GL_ARB_occlusion_query
  GL_ARB_pixel_buffer_object             GL_ARB_point_parameters
  GL_ARB_point_sprite                    GL_ARB_provoking_vertex
  GL_ARB_seamless_cube_map               GL_ARB_shader_objects
  GL_ARB_shader_texture_lod              GL_ARB_shading_language_100
  GL_ARB_shadow                          GL_ARB_shadow_ambient
  GL_ARB_sync                            GL_ARB_texture_border_clamp
  GL_ARB_texture_compression             GL_ARB_texture_compression_rgtc
  GL_ARB_texture_cube_map                GL_ARB_texture_env_add
  GL_ARB_texture_env_combine             GL_ARB_texture_env_crossbar
  GL_ARB_texture_env_dot3                GL_ARB_texture_float
  GL_ARB_texture_mirrored_repeat         GL_ARB_texture_non_power_of_two
  GL_ARB_texture_rectangle               GL_ARB_texture_rg
  GL_ARB_transpose_matrix                GL_ARB_vertex_array_bgra
  GL_ARB_vertex_blend                    GL_ARB_vertex_buffer_object
  GL_ARB_vertex_program                  GL_ARB_vertex_shader
  GL_ARB_window_pos                      GL_ATI_separate_stencil
  GL_ATI_texture_compression_3dc         GL_ATI_texture_env_combine3
  GL_ATI_texture_float                   GL_ATI_texture_mirror_once
  GL_EXT_abgr                            GL_EXT_bgra
  GL_EXT_bindable_uniform                GL_EXT_blend_color
  GL_EXT_blend_equation_separate         GL_EXT_blend_func_separate
  GL_EXT_blend_minmax                    GL_EXT_blend_subtract
  GL_EXT_clip_volume_hint                GL_EXT_debug_label
  GL_EXT_debug_marker                    GL_EXT_depth_bounds_test
  GL_EXT_draw_buffers2                   GL_EXT_draw_range_elements
  GL_EXT_fog_coord                       GL_EXT_framebuffer_blit
  GL_EXT_framebuffer_multisample         GL_EXT_framebuffer_multisample_blit_scaled
  GL_EXT_framebuffer_object              GL_EXT_framebuffer_sRGB
  GL_EXT_geometry_shader4                GL_EXT_gpu_program_parameters
  GL_EXT_gpu_shader4                     GL_EXT_multi_draw_arrays
  GL_EXT_packed_depth_stencil            GL_EXT_packed_float
  GL_EXT_provoking_vertex                GL_EXT_rescale_normal
  GL_EXT_secondary_color                 GL_EXT_separate_specular_color
  GL_EXT_shadow_funcs                    GL_EXT_stencil_two_side
  GL_EXT_stencil_wrap                    GL_EXT_texture_array
  GL_EXT_texture_compression_dxt1        GL_EXT_texture_compression_s3tc
  GL_EXT_texture_env_add                 GL_EXT_texture_filter_anisotropic
  GL_EXT_texture_integer                 GL_EXT_texture_lod_bias
  GL_EXT_texture_mirror_clamp            GL_EXT_texture_rectangle
  GL_EXT_texture_sRGB                    GL_EXT_texture_sRGB_decode
  GL_EXT_texture_shared_exponent         GL_EXT_timer_query
  GL_EXT_transform_feedback              GL_EXT_vertex_array_bgra
  GL_IBM_rasterpos_clip                  GL_NV_blend_square
  GL_NV_conditional_render               GL_NV_depth_clamp
  GL_NV_fog_distance                     GL_NV_light_max_exponent
  GL_NV_texgen_reflection                GL_NV_texture_barrier
  GL_SGIS_generate_mipmap                GL_SGIS_texture_edge_clamp
  GL_SGIS_texture_lod
:display:gsg:glgsg(debug): HAS EXT GL_EXT_debug_marker 1
:display:gsg:glgsg(debug): HAS EXT GL_KHR_debug 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_debug_output 0
:display:gsg:glgsg(debug): gl-debug disabled and unsupported.
:display:gsg:glgsg(debug): HAS EXT GL_ARB_ES3_compatibility 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_texture_storage 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_clear_texture 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_clear_buffer_object 0
:display:gsg:glgsg(debug): HAS EXT GL_EXT_texture_array 1
:display:gsg:glgsg(debug): HAS EXT GL_ARB_seamless_cube_map 1
:display:gsg:glgsg(debug): HAS EXT GL_ARB_texture_cube_map_array 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_texture_buffer_object 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_texture_compression_rgtc 1
:display:gsg:glgsg(debug): HAS EXT GL_ARB_vertex_array_bgra 1
:display:gsg:glgsg(debug): HAS EXT GL_ARB_vertex_type_10f_11f_11f_rev 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_framebuffer_object 1
:display:gsg:glgsg(debug): HAS EXT GL_ARB_map_buffer_range 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_buffer_storage 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_vertex_array_object 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_tessellation_shader 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_geometry_shader4 0
:display:gsg:glgsg(debug): HAS EXT GL_EXT_geometry_shader4 1
:display:gsg:glgsg(debug): HAS EXT GL_ARB_compute_shader 0
:display:gsg:glgsg(debug): HAS EXT GL_EXT_gpu_shader4 1
:display:gsg:glgsg(debug): HAS EXT GL_ARB_vertex_attrib_64bit 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_vertex_attrib_binding 0
:display:gsg:glgsg(debug): HAS EXT ARB_shader_storage_buffer_object 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_instanced_arrays 1
:display:gsg:glgsg(debug): HAS EXT GL_ARB_draw_instanced 1
:display:gsg:glgsg(debug): HAS EXT GL_ARB_draw_indirect 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_framebuffer_object 1
:display:gsg:glgsg(debug): HAS EXT GL_ARB_direct_state_access 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_framebuffer_no_attachments 0
:display:gsg:glgsg(debug): HAS EXT GL_NV_framebuffer_multisample_coverage 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_viewport_array 0
:display:gsg:glgsg(debug): Occlusion query counter provides 32 bits.
:display:gsg:glgsg(debug): HAS EXT GL_ARB_timer_query 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_blend_func_extended 0
:display:gsg:glgsg(debug): HAS EXT GL_EXT_texture_mirror_clamp 1
:display:gsg:glgsg(debug): max texture dimension = 16384, max 3d texture = 16384, max 2d texture array = 16384, max cube map = 16384
:display:gsg:glgsg(debug): max_elements_vertices = 1048575, max_elements_indices = 150000
:display:gsg:glgsg(debug): vertex buffer objects are supported.
:display:gsg:glgsg(debug): Supported compressed texture formats:
  GL_COMPRESSED_RGB_S3TC_DXT1_EXT
  GL_COMPRESSED_RGBA_S3TC_DXT3_EXT
  GL_COMPRESSED_RGBA_S3TC_DXT5_EXT
  Unknown compressed format 0x8837
:display:gsg:glgsg(debug): HAS EXT GL_EXT_texture_filter_anisotropic 1
:display:gsg:glgsg(debug): HAS EXT GL_ARB_shader_image_load_store 0
:display:gsg:glgsg(debug): HAS EXT GL_EXT_shader_image_load_store 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_sampler_objects 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_multi_bind 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_internalformat_query2 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_bindless_texture 0
:display:gsg:glgsg(debug): HAS EXT GL_ARB_get_program_binary 0
:display:gsg:glgsg(debug): HAS EXT GL_EXT_stencil_two_side 1
:display:gsg:glgsg(debug): max lights = 8
:display:gsg:glgsg(debug): max clip planes = 6
:display:gsg:glgsg(debug): max texture stages = 8
:display:gsg:glgsg(debug): No program binary formats supported.
:display:gsg:glgsg(debug): HAS EXT GL_NV_gpu_program5 0
:display:gsg:glgsg(debug): HAS EXT GL_NV_gpu_program4 0
:display:gsg:glgsg(debug): HAS EXT GL_NV_fragment_program2 0
:display:gsg:glgsg(debug): HAS EXT GL_NV_fragment_program 0
:display:gsg:glgsg(debug): shader model = 2.0
:display:gsg:glgsg(debug): Creating depth stencil renderbuffer.
:display:gsg:glgsg(debug): Creating color renderbuffer.
:display:gsg:glgsg(error): GL error 0x500 : invalid enumerant
:display:gsg:glgsg(error): An OpenGL error has occurred.  Set gl-check-errors #t in your PRC file to display more information.
:display:gsg:glgsg(debug): Creating depth stencil renderbuffer.
:display:gsg:glgsg(debug): Creating color renderbuffer.
:display:gsg:glgsg(debug): Creating depth stencil renderbuffer.
:display:gsg:glgsg(debug): Creating color renderbuffer.
:display:gsg:glgsg(debug): Compiling GLSL vertex shader pbr
:display:gsg:glgsg(debug): Compiling GLSL fragment shader pbr
:display:gsg:glgsg(debug): Linking GLSL shader pbr
:display:gsg:glgsg(debug): Active attribute p3d_MultiTexCoord0 with size 1 and type 0x8b50 is bound to location 1
:display:gsg:glgsg(debug): Active attribute p3d_Vertex with size 1 and type 0x8b52 is bound to location 0
:display:gsg:glgsg(debug): Active attribute p3d_Tangent with size 1 and type 0x8b52 is bound to location 4
:display:gsg:glgsg(debug): Active attribute p3d_Color with size 1 and type 0x8b52 is bound to location 3
:display:gsg:glgsg(debug): Active attribute p3d_Normal with size 1 and type 0x8b51 is bound to location 2
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[0].position with size 1 and type 0x8b52 is bound to location 44
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[0].diffuse with size 1 and type 0x8b52 is bound to location 45
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[0].specular with size 1 and type 0x8b52 is bound to location 46
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[0].attenuation with size 1 and type 0x8b51 is bound to location 47
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[0].spotDirection with size 1 and type 0x8b51 is bound to location 48
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[0].spotCosCutoff with size 1 and type 0x1406 is bound to location 49
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[0].shadowMap with size 1 and type 0x8b62 is bound to location 50
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[0].shadowViewMatrix with size 1 and type 0x8b5c is bound to location 51
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[1].position with size 1 and type 0x8b52 is bound to location 55
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[1].diffuse with size 1 and type 0x8b52 is bound to location 56
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[1].specular with size 1 and type 0x8b52 is bound to location 57
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[1].attenuation with size 1 and type 0x8b51 is bound to location 58
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[1].spotDirection with size 1 and type 0x8b51 is bound to location 59
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[1].spotCosCutoff with size 1 and type 0x1406 is bound to location 60
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[1].shadowMap with size 1 and type 0x8b62 is bound to location 61
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[1].shadowViewMatrix with size 1 and type 0x8b5c is bound to location 62
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[2].position with size 1 and type 0x8b52 is bound to location 66
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[2].diffuse with size 1 and type 0x8b52 is bound to location 67
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[2].specular with size 1 and type 0x8b52 is bound to location 68
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[2].attenuation with size 1 and type 0x8b51 is bound to location 69
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[2].spotDirection with size 1 and type 0x8b51 is bound to location 70
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[2].spotCosCutoff with size 1 and type 0x1406 is bound to location 71
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[2].shadowMap with size 1 and type 0x8b62 is bound to location 72
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[2].shadowViewMatrix with size 1 and type 0x8b5c is bound to location 73
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[3].position with size 1 and type 0x8b52 is bound to location 77
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[3].diffuse with size 1 and type 0x8b52 is bound to location 78
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[3].specular with size 1 and type 0x8b52 is bound to location 79
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[3].attenuation with size 1 and type 0x8b51 is bound to location 80
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[3].spotDirection with size 1 and type 0x8b51 is bound to location 81
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[3].spotCosCutoff with size 1 and type 0x1406 is bound to location 82
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[3].shadowMap with size 1 and type 0x8b62 is bound to location 83
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[3].shadowViewMatrix with size 1 and type 0x8b5c is bound to location 84
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[4].position with size 1 and type 0x8b52 is bound to location 88
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[4].diffuse with size 1 and type 0x8b52 is bound to location 89
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[4].specular with size 1 and type 0x8b52 is bound to location 90
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[4].attenuation with size 1 and type 0x8b51 is bound to location 91
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[4].spotDirection with size 1 and type 0x8b51 is bound to location 92
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[4].spotCosCutoff with size 1 and type 0x1406 is bound to location 93
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[4].shadowMap with size 1 and type 0x8b62 is bound to location 94
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[4].shadowViewMatrix with size 1 and type 0x8b5c is bound to location 95
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[5].position with size 1 and type 0x8b52 is bound to location 99
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[5].diffuse with size 1 and type 0x8b52 is bound to location 100
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[5].specular with size 1 and type 0x8b52 is bound to location 101
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[5].attenuation with size 1 and type 0x8b51 is bound to location 102
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[5].spotDirection with size 1 and type 0x8b51 is bound to location 103
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[5].spotCosCutoff with size 1 and type 0x1406 is bound to location 104
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[5].shadowMap with size 1 and type 0x8b62 is bound to location 105
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[5].shadowViewMatrix with size 1 and type 0x8b5c is bound to location 106
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[6].position with size 1 and type 0x8b52 is bound to location 110
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[6].diffuse with size 1 and type 0x8b52 is bound to location 111
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[6].specular with size 1 and type 0x8b52 is bound to location 112
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[6].attenuation with size 1 and type 0x8b51 is bound to location 113
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[6].spotDirection with size 1 and type 0x8b51 is bound to location 114
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[6].spotCosCutoff with size 1 and type 0x1406 is bound to location 115
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[6].shadowMap with size 1 and type 0x8b62 is bound to location 116
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[6].shadowViewMatrix with size 1 and type 0x8b5c is bound to location 117
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[7].position with size 1 and type 0x8b52 is bound to location 121
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[7].diffuse with size 1 and type 0x8b52 is bound to location 122
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[7].specular with size 1 and type 0x8b52 is bound to location 123
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[7].attenuation with size 1 and type 0x8b51 is bound to location 124
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[7].spotDirection with size 1 and type 0x8b51 is bound to location 125
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[7].spotCosCutoff with size 1 and type 0x1406 is bound to location 126
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[7].shadowMap with size 1 and type 0x8b62 is bound to location 127
:display:gsg:glgsg(debug): Active uniform p3d_LightSource[7].shadowViewMatrix with size 1 and type 0x8b5c is bound to location 128
:display:gsg:glgsg(debug): Active uniform p3d_TextureModulate with size 1 and type 0x8b5e is bound to location 3
:display:gsg:glgsg(debug): Active uniform brdf_lut with size 1 and type 0x8b5e is bound to location 4
:display:gsg:glgsg(debug): Active uniform max_reflection_lod with size 1 and type 0x1406 is bound to location 5
:display:gsg:glgsg(debug): Active uniform p3d_ModelViewMatrix with size 1 and type 0x8b5c is bound to location 6
:display:gsg:glgsg(debug): Active uniform p3d_ModelMatrixInverseTranspose with size 1 and type 0x8b5c is bound to location 10
:display:gsg:glgsg(debug): Active uniform p3d_ModelMatrix with size 1 and type 0x8b5c is bound to location 14
:display:gsg:glgsg(debug): Active uniform p3d_LightModel.ambient with size 1 and type 0x8b52 is bound to location 19
:display:gsg:glgsg(debug): Active uniform p3d_TextureSelector with size 1 and type 0x8b5e is bound to location 18
:display:gsg:glgsg(debug): Active uniform sh_coeffs[0] with size 9 and type 0x8b51 is bound to location 20
:display:gsg:glgsg(debug): Active uniform p3d_Material.baseColor with size 1 and type 0x8b52 is bound to location 29
:display:gsg:glgsg(debug): Active uniform p3d_Material.emission with size 1 and type 0x8b52 is bound to location 30
:display:gsg:glgsg(debug): Active uniform p3d_Material.roughness with size 1 and type 0x1406 is bound to location 31
:display:gsg:glgsg(debug): Active uniform p3d_Material.metallic with size 1 and type 0x1406 is bound to location 32
:display:gsg:glgsg(debug): Active uniform p3d_TexAlphaOnly with size 1 and type 0x8b52 is bound to location 33
:display:gsg:glgsg(debug): Active uniform p3d_ProjectionMatrix with size 1 and type 0x8b5c is bound to location 34
:display:gsg:glgsg(debug): Active uniform filtered_env_map with size 1 and type 0x8b60 is bound to location 38
:display:gsg:glgsg(debug): Active uniform p3d_ColorScale with size 1 and type 0x8b52 is bound to location 39
:display:gsg:glgsg(debug): Active uniform p3d_TextureMatrix with size 1 and type 0x8b5c is bound to location 40
:display:gsg:glgsg(debug): Active uniform p3d_NormalMatrix with size 1 and type 0x8b5b is bound to location 0
:display:gsg:glgsg(debug): loading texture with NULL image dummy-shadow-2d
:display:gsg:glgsg(debug): loading new texture object for dummy-shadow-2d, 1 x 1 x 1, z = 0, mipmaps 0, uses_mipmaps = 0
:display:gsg:glgsg(debug):   (initializing NULL image)
:display:gsg:glgsg(debug): loading texture with NULL image filtered_env_map
:display:gsg:glgsg(debug): loading new texture object for filtered_env_map, 1 x 1 x 6, z = 0, mipmaps 0, uses_mipmaps = 0
:display:gsg:glgsg(debug):   (initializing NULL image)
:display:gsg:glgsg(debug): loading new texture object for filtered_env_map, 1 x 1 x 6, z = 1, mipmaps 1, uses_mipmaps = 0
:display:gsg:glgsg(debug): loading new texture object for filtered_env_map, 1 x 1 x 6, z = 2, mipmaps 1, uses_mipmaps = 0
:display:gsg:glgsg(debug): loading new texture object for filtered_env_map, 1 x 1 x 6, z = 3, mipmaps 1, uses_mipmaps = 0
:display:gsg:glgsg(debug): loading new texture object for filtered_env_map, 1 x 1 x 6, z = 4, mipmaps 1, uses_mipmaps = 0
:display:gsg:glgsg(debug): loading new texture object for filtered_env_map, 1 x 1 x 6, z = 5, mipmaps 1, uses_mipmaps = 0
:display:gsg:glgsg(debug): loading texture with NULL image Directional Light01
:display:gsg:glgsg(debug): loading new texture object for Directional Light01, 4096 x 4096 x 1, z = 0, mipmaps 0, uses_mipmaps = 0
:display:gsg:glgsg(debug):   (initializing NULL image)
:display:gsg:glgsg(debug): Binding texture Directional Light01 to depth attachment.
:display:gsg:glgsg(debug): Creating color renderbuffer.
:display:gsg:glgsg(debug): Binding texture Directional Light01 to depth attachment.
:display:gsg:glgsg(debug): Creating color renderbuffer.

Okay, this explains the difference. If I run your code with the force_software framebuffer property set, I get the same renderer, and the same issue. Sometimes the skybox is entirely grey, sometimes it renders with the partial pattern (looks almost like z-fighting?). Hopefully this gives you a starting point for further experimentation.

Note that the shader generator does not work on ARM macs (except via x86_64 emulation), this will be fixed in 1.11. But it means the set_shader_auto call is in this case equivalent to set_shader_off.

You could try writing and applying a minimal GLSL shader for rendering the skybox.

If I run your code with the force_software framebuffer property set, I get the same renderer, and the same issue.

Ah amazing ! I was suspecting the issue was related to software rendering. I’m facing other side-effects then running such scripts on the CI because of this, typically anti-aliasing and/or MSAA not working, no shadow… but I never faced of rendering issue where “random” parts of the sky are missing…

You could try writing and applying a minimal GLSL shader for rendering the skybox.

Hum… I guess I could. By I have absolutely no idea what it means x) I have very basic knowledge in computer graphics and what I’m trying to do with Panda3d is pretty limited for now as you can see. Even for the sky box I needed help from this forum x)

By the way, why does software rendering is working just fine on Intel x86 ? it is related to what you said about the shader generator not working on ARM macs ?

Yes, it could be–try replacing set_shader_auto with set_shader_off and see if you get the same issue on Intel.

It could also be due to Apple having rewritten their OpenGL drivers for ARM macs.

Hum… I don’t really understand what is happening here. I replaced set_shader_auto with set_shader_off. If I do not force software rendering everything is fine, but if I do, the background is grey instead of displaying the sky box. I NEVER see glitches where parts of the sky box are properly rendered.

I managed to write a minimum reproducible example:

import array
from math import pi, sin, cos

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase


def create_gradient(
        subdiv, offset, sky_color, ground_color, top_color, bottom_color):
    subdiv = max(1, subdiv)
    offset = max(0., min(1., offset))
    color1 = tuple(int(round(c * 255)) for c in sky_color)
    color2 = tuple(int(round(c * 255)) for c in ground_color)
    top_col = tuple(int(round(c * 255)) for c in top_color)
    btm_col = tuple(int(round(c * 255)) for c in bottom_color)

    vertex_format = GeomVertexFormat()
    array_format = GeomVertexArrayFormat()
    array_format.add_column(InternalName.get_vertex(), 3, Geom.NT_float32, Geom.C_point)
    vertex_format.add_array(array_format)
    array_format = GeomVertexArrayFormat()
    array_format.add_column(InternalName.make("color"), 4, Geom.NT_uint8, Geom.C_color)
    vertex_format.add_array(array_format)
    vertex_format = GeomVertexFormat.register_format(vertex_format)

    vertex_data = GeomVertexData("prism_data", vertex_format, GeomEnums.UH_static)
    vertex_data.unclean_set_num_rows(4 + subdiv * 2)
    values = array.array("f", (-1000., -50., 86.6, 1000., -50., 86.6))
    offset_angle = pi / 1.5 * offset
    delta_angle = (pi / .75 - offset_angle * 2.) / (subdiv + 1)
    for i in range(subdiv):
        angle = pi / 3. + offset_angle + delta_angle * (i + 1)
        y = -cos(angle) * 100.
        z = sin(angle) * 100.
        values.extend((-1000., y, z, 1000., y, z))
    values.extend((-1000., -50., -86.6, 1000., -50., -86.6))
    pos_array = vertex_data.modify_array(0)
    memview = memoryview(pos_array).cast("B").cast("f")
    memview[:] = values
    values = array.array("B", top_col * 2)
    s = subdiv + 1

    # interpolate the colors
    for i in range(subdiv):
        f1 = (subdiv - i) / s
        f2 = 1. - f1
        color = tuple(int(round(c1 * f1 + c2 * f2))
            for c1, c2 in zip(color1, color2))
        values.extend(color * 2)
    values.extend(btm_col * 2)
    color_array = vertex_data.modify_array(1)
    memview = memoryview(color_array).cast("B")
    memview[:] = values

    tris_prim = GeomTriangles(GeomEnums.UH_static)
    indices = array.array("H", (0, 3, 1, 0, 2, 3))
    for i in range(subdiv + 1):
        j = i * 2
        indices.extend((j, 3 + j, 1 + j, j, 2 + j, 3 + j))
    j = (subdiv + 1) * 2
    indices.extend((j, 1, 1 + j, j, 0, 1))
    tris_array = tris_prim.modify_vertices()
    tris_array.unclean_set_num_rows((subdiv + 3) * 6)
    memview = memoryview(tris_array).cast("B").cast("H")
    memview[:] = indices

    geom = Geom(vertex_data)
    geom.add_primitive(tris_prim)
    node = GeomNode("prism")
    node.add_geom(geom)
    prism = NodePath(node)

    return prism


class MyApp(ShowBase):
    def __init__(self):
        loadPrcFileData('', """
            framebuffer-software 1
            framebuffer-hardware 0
            """)
        super().__init__()

        self.background_gradient = create_gradient(
            2, .75, (0, 0, 1., 1.), (0, 1., 0, 1.), (1., 1., 1., 1.), (0., .5, 0., 1.))
        self.background_gradient.reparent_to(self.render)


app = MyApp()
app.run()

OK maybe I found what is causing the issue, but I have no clue what is the actual root cause and why it is related to software rendering on Apple Silicon in particular… It seems to be related to the dimension of the skybox:

    vertex_data = GeomVertexData("prism_data", vertex_format, GeomEnums.UH_static)
    vertex_data.unclean_set_num_rows(4 + subdiv * 2)
    values = array.array("f", (-1000., -50., 86.6, 1000., -50., 86.6))
    offset_angle = pi / 1.5 * offset
    delta_angle = (pi / .75 - offset_angle * 2.) / (subdiv + 1)
    for i in range(subdiv):
        angle = pi / 3. + offset_angle + delta_angle * (i + 1)
        y = -cos(angle) * 100.
        z = sin(angle) * 100.
        values.extend((-1000., y, z, 1000., y, z))
    values.extend((-1000., -50., -86.6, 1000., -50., -86.6))
    pos_array = vertex_data.modify_array(0)
    memview = memoryview(pos_array).cast("B").cast("f")
    memview[:] = values
    values = array.array("B", top_col * 2)
    s = subdiv + 1

If I replace 1000 by 100, now the sky box is sometimes rendered properly.

Edit: I just realised that polygons of the skybox are properly rendered if all their vertices are inside the field of view of the camera, otherwise it is random.

Hi @rdb , sorry to bother you. Do you think there is anything that could be done to fix the issue ? I need to move away from ‘macos-13’ because it is going to be deprecated sooner than later, so I would like to know whether I should consider disabling the whole skybox thing completely in case of software rendering on MacOS or a fix may come at some point.

Actually, I had the same issue on Mac OS 14. That is, when “framebuffer-hardware false” and “framebuffer-software true” are active, big triangles that are partly out of view are glitched.

I found that when additionally “load-display p3tinydisplay” is in the config then the scene is rendered largely correctly. I say largely, because there is another small issue that turns up - there is some visible flickering (z-fighting) in some geometry that is very close to each other (planes closely on top of each other) that is not there when either the hardware framebuffer or the Apple software framebuffer is active.

I worked a bit on stripping it down to a more minimal test case and I think it’s indeed got to do with the triangles being really large (specifically, long and thin) and partially out of view. I think it’s a clipping bug in the driver.

You could probably circumvent this by tessellating the skybox mesh a bit more. Alternatively, changing the -1000 and 1000 to -30 and 30, or some lower values like that.

It’s possible that this is due to the z-buffer having lower precision (16-bit depth) in tinydisplay. You could try bringing your near and far clip planes closer together. In particular, raising the near distance will have the greatest effect.

I quickly hacked in some horizontal tessellation into your skybox generation code, which produces triangles with reasonable aspect ratio and suppresses the glitches:

    segments = 30

    # Define vertex format
    vformat = GeomVertexFormat()
    aformat = GeomVertexArrayFormat()
    aformat.add_column(
        InternalName.get_vertex(), 3, Geom.NT_float32, Geom.C_point)
    vformat.add_array(aformat)
    aformat = GeomVertexArrayFormat()
    aformat.add_column(
        InternalName.make("color"), 4, Geom.NT_uint8, Geom.C_color)
    vformat.add_array(aformat)
    vformat = GeomVertexFormat.register_format(vformat)

    # Create a simple, horizontal prism.
    # Make it very wide to avoid ever seeing its left and right sides.
    # One edge is at the "horizon", while the two other edges are above
    # and a bit behind the camera so they are only visible when looking
    # straight up. ((-50., 86.6) -> (cos(2*pi/3), sin(2*pi/3)))
    vertex_data = GeomVertexData(
        "prism_data", vformat, GeomEnums.UH_static)
    vertex_data.unclean_set_num_rows((4 + subdiv * 2) * segments)

    values = array.array("f", ())
    for hi in range(segments):
        t0 = hi / segments
        t1 = (hi + 1) / segments
        x0 = -1000. * (1 - t0) + 1000. * t0
        x1 = -1000. * (1 - t1) + 1000. * t1

        values.extend((x0, -50., 86.6, x1, -50., 86.6))
        offset_angle = np.pi / 1.5 * (1.0 - span)
        delta_angle = 2. * (np.pi / 1.5 - offset_angle) / (subdiv + 1)
        for i in range(subdiv):
            angle = np.pi / 3. + offset + offset_angle + delta_angle * (i + 1)
            y = -np.cos(angle) * 100.
            z = np.sin(angle) * 100.
            values.extend((x0, y, z, x1, y, z))
        values.extend((x0, -50., -86.6, x1, -50., -86.6))

    pos_array = vertex_data.modify_array(0)
    memview = memoryview(pos_array).cast("B").cast("f")
    memview[:] = values

    # Interpolate the colors
    color1 = tuple(int(c * 255) for c in sky_color)
    color2 = tuple(int(c * 255) for c in ground_color)
    values = array.array("B", ())
    for hi in range(segments):
        values.extend(color1 * 2)
        for ratio in np.linspace(0, 1, subdiv):
            color = tuple(int(c1 * (1 - ratio) + c2 * ratio)
                          for c1, c2 in zip(color1, color2))
            values.extend(color * 2)
        values.extend(color2 * 2)
    color_array = vertex_data.modify_array(1)
    memview = memoryview(color_array).cast("B")
    memview[:] = values

    tris_prim = GeomTriangles(GeomEnums.UH_static)
    indices = array.array("H", ())
    for hi in range(segments):
        off = (subdiv + 2) * 2 * hi
        indices.extend((off, off + 3, off + 1, off, off + 2, off + 3))
        for i in range(subdiv + 1):
            j = i * 2 + off
            indices.extend((j, 3 + j, 1 + j, j, 2 + j, 3 + j))
        j = (subdiv + 1) * 2 + off
        indices.extend((j, off + 1, 1 + j, j, off, off + 1))

    tris_array = tris_prim.modify_vertices()
    tris_array.unclean_set_num_rows((subdiv + 3) * 6 * segments)
    memview = memoryview(tris_array).cast("B").cast("H")
    memview[:] = indices

Still, I would suggest tweaking the high -1000/1000 values and lowering them, which would also allow you to lower the segment count.

I’m not really sure there’s anything we can do on the Panda end to work around the driver bug, unfortunately. As suggested, using the tinydisplay renderer, which we fully control, may be an option.

@rdb thank you for your help. It was very helpful. I will reduce the width from 1000 to 100, which is enough in practice. I need to check but I think it must be somewhat large, otherwise strange visual glitches appear when zooming out too much (x-fighting ?). In this case, 4 “segments” should be enough based on your analysis. Do you think that switching to tinydisplay would be a better options ? i.e. it has all the same feature while being less buggy (slower is not a big deal in this case) ?

For the record, here is a thread about the many issues related to software rendering on Apple Silicon in the context of GitHub Action. The conclusion is the same as yours: apple driver bug. Spoiler alert, it was not reported to apple, so no fix in sight…

I don’t have much experience with Apple’s software renderer, so I can’t really comment on how reliable it is. I know that tinydisplay is quite limited in feature set. If its limited feature set is sufficient for your use cases, then you will probably get more consistent results with it.

It might also be possible to use an alternative, thirdparty software OpenGL implementation that is of higher quality (such as Mesa), though that probably requires creating a special build of Panda3D and some modifications may be necessary.

Ok so I think I will rather fallback to tinydisplay on MacOS when only Apple Software Renderer is available. Is there a way to check this with panda3d ? Because I don’t want to set load-display p3tinydisplay systematically.

I’m not aware of a way to detect it ahead of time, but you could set the force_hardware flag without setting force_software, and if the buffer fails to open, re-open it with p3tinydisplay (but unsetting force_hardware).

OK I see. Thank you for your feedback. I ended up simply adding an environment variable to force tinydisplay. Since in particle this is only necessary for Github Action due to nested virtualisation limitations, it is not worth the effort.