| import struct |
| from .shader_compiler import ShaderCompiler |
| from .rasterizer import Rasterizer |
|
|
| class GraphicsAPI: |
| def __init__(self, driver): |
| self.driver = driver |
| self._verify_driver_compatibility() |
| |
| |
| self.buffers = {} |
| self.textures = {} |
| self.framebuffers = {} |
| self.programs = {} |
| |
| |
| self.current_program = None |
| self.current_framebuffer = None |
| self.current_texture_units = {} |
| |
| |
| self.shader_compiler = ShaderCompiler() |
| self.rasterizer = Rasterizer(driver) |
| |
| print("Graphics API initialized.") |
| |
| def _verify_driver_compatibility(self): |
| """Verify driver has required graphics capabilities""" |
| required_methods = [ |
| 'create_buffer', |
| 'create_texture', |
| 'create_framebuffer', |
| 'draw_triangles' |
| ] |
| |
| missing_methods = [method for method in required_methods |
| if not hasattr(self.driver, method)] |
| |
| if missing_methods: |
| raise RuntimeError( |
| f"Driver missing required graphics methods: {missing_methods}" |
| "\nEnsure driver has modern graphics API support." |
| ) |
| |
| |
| required_enums = [ |
| 'TextureFormat', |
| 'FilterMode', |
| 'WrapMode', |
| 'BufferType', |
| 'MSAASamples' |
| ] |
| |
| missing_enums = [enum for enum in required_enums |
| if not hasattr(self.driver, enum)] |
| |
| if missing_enums: |
| raise RuntimeError( |
| f"Driver missing required graphics enums: {missing_enums}" |
| "\nEnsure driver has modern graphics type support." |
| ) |
|
|
| def create_buffer(self, data, buffer_type="vertex", dynamic=False, map_write=False): |
| """ |
| Create a new buffer with data |
| |
| Args: |
| data: numpy array or bytes of buffer data |
| buffer_type: "vertex", "index", "uniform", "storage", or "indirect" |
| dynamic: Whether buffer will be frequently updated |
| map_write: Whether buffer should be mappable for CPU writes |
| """ |
| if not self.driver.initialized: |
| raise RuntimeError("Driver not initialized.") |
| |
| |
| type_map = { |
| "vertex": self.driver.BufferType.VERTEX, |
| "index": self.driver.BufferType.INDEX, |
| "uniform": self.driver.BufferType.UNIFORM, |
| "storage": self.driver.BufferType.STORAGE, |
| "indirect": self.driver.BufferType.INDIRECT |
| } |
| |
| if buffer_type not in type_map: |
| raise ValueError(f"Invalid buffer type: {buffer_type}") |
| |
| |
| buffer_id = self.driver.create_buffer( |
| data=data, |
| buffer_type=type_map[buffer_type], |
| dynamic=dynamic, |
| map_write=map_write |
| ) |
| |
| |
| self.buffers[buffer_id] = { |
| "type": buffer_type, |
| "size": len(data), |
| "dynamic": dynamic, |
| "map_write": map_write |
| } |
| |
| print(f"Created {buffer_type} buffer with ID {buffer_id}, size {len(data)} bytes.") |
| return buffer_id |
|
|
| def delete_buffer(self, buffer_id): |
| if buffer_id in self.buffers: |
| buffer_info = self.buffers[buffer_id] |
| self.driver.free_memory(buffer_info["virtual_address"]) |
| del self.buffers[buffer_id] |
| print(f"Deleted buffer with ID {buffer_id}.") |
| else: |
| print(f"Warning: Attempted to delete non-existent buffer with ID {buffer_id}.") |
|
|
| def buffer_data(self, buffer_id, data): |
| if buffer_id not in self.buffers: |
| raise ValueError(f"Buffer with ID {buffer_id} not found.") |
| |
| buffer_info = self.buffers[buffer_id] |
| if len(data) > buffer_info["size"]: |
| raise ValueError(f"Data size ({len(data)}) exceeds buffer size ({buffer_info['size']}) for buffer ID {buffer_id}.") |
|
|
| self.driver.write_memory(buffer_info["virtual_address"], data) |
| print(f"Loaded {len(data)} bytes into buffer ID {buffer_id}.") |
|
|
| def draw_arrays(self, mode, first, count, vertex_buffer, |
| primitive_restart_index=None, instances=1): |
| """ |
| Draw primitives from vertex buffer |
| |
| Args: |
| mode: "points", "lines", "triangles", etc |
| first: First vertex to draw |
| count: Number of vertices |
| vertex_buffer: Vertex buffer ID |
| primitive_restart_index: Index value that restarts primitive |
| instances: Number of instances to draw |
| """ |
| if not self.driver.initialized: |
| raise RuntimeError("Driver not initialized.") |
| if not self.current_program: |
| raise RuntimeError("No shader program in use.") |
| if not self.current_framebuffer: |
| raise RuntimeError("No framebuffer bound.") |
| |
| print(f"Drawing {count} vertices in {mode} mode") |
| |
| |
| self.driver.draw_triangles( |
| vertex_buffer_id=vertex_buffer, |
| index_buffer_id=None, |
| framebuffer_id=self.current_framebuffer, |
| shader_program=self.current_program, |
| num_vertices=count, |
| start_vertex=first |
| ) |
| |
| |
| self.driver.add_command("draw_arrays", |
| mode=mode, |
| first=first, |
| count=count, |
| vertex_buffer=vertex_buffer, |
| primitive_restart_index=primitive_restart_index, |
| instances=instances |
| ) |
|
|
| def draw_indexed(self, mode, count, vertex_buffer, index_buffer, |
| index_offset=0, base_vertex=0, instances=1, |
| primitive_restart_index=None): |
| """ |
| Draw indexed primitives |
| |
| Args: |
| mode: "points", "lines", "triangles", etc |
| count: Number of indices |
| vertex_buffer: Vertex buffer ID |
| index_buffer: Index buffer ID |
| index_offset: Starting offset in index buffer |
| base_vertex: Value added to each index |
| instances: Number of instances to draw |
| primitive_restart_index: Index value that restarts primitive |
| """ |
| if not self.driver.initialized: |
| raise RuntimeError("Driver not initialized.") |
| if not self.current_program: |
| raise RuntimeError("No shader program in use.") |
| if not self.current_framebuffer: |
| raise RuntimeError("No framebuffer bound.") |
| |
| if index_buffer not in self.buffers or self.buffers[index_buffer]["type"] != "index": |
| raise ValueError(f"Invalid index buffer ID: {index_buffer}") |
| |
| print(f"Drawing {count} indices in {mode} mode") |
| |
| |
| self.driver.draw_triangles( |
| vertex_buffer_id=vertex_buffer, |
| index_buffer_id=index_buffer, |
| framebuffer_id=self.current_framebuffer, |
| shader_program=self.current_program, |
| num_vertices=count, |
| start_vertex=index_offset |
| ) |
| |
| |
| self.driver.add_command("draw_indexed", |
| mode=mode, |
| count=count, |
| vertex_buffer=vertex_buffer, |
| index_buffer=index_buffer, |
| index_offset=index_offset, |
| base_vertex=base_vertex, |
| instances=instances, |
| primitive_restart_index=primitive_restart_index |
| ) |
|
|
| def compile_shader(self, shader_source, shader_type="vertex"): |
| return self.shader_compiler.compile_shader(shader_source, shader_type) |
|
|
| def link_program(self, vertex_shader, fragment_shader): |
| return self.shader_compiler.link_program(vertex_shader, fragment_shader) |
|
|
| def use_program(self, program): |
| if not self.shader_compiler.validate_program(program): |
| raise ValueError("Invalid shader program.") |
| self.current_program = program |
| print(f"Using shader program: {program['id']}.") |
|
|
| def create_texture(self, width, height, format="rgba8", |
| filter="bilinear", wrap="repeat", |
| generate_mipmaps=True, aniso_level=1): |
| """ |
| Create a new texture |
| |
| Args: |
| width, height: Texture dimensions |
| format: "r8", "rgba8", "rgba16f", "rgba32f", etc. |
| filter: "nearest", "bilinear", "trilinear", "anisotropic" |
| wrap: "repeat", "clamp", "mirror" |
| generate_mipmaps: Whether to generate mipmaps |
| aniso_level: Anisotropic filtering level (1-16) |
| """ |
| |
| format_map = { |
| "r8": self.driver.TextureFormat.R8, |
| "rg8": self.driver.TextureFormat.RG8, |
| "rgba8": self.driver.TextureFormat.RGBA8, |
| "r16f": self.driver.TextureFormat.R16F, |
| "rgba16f": self.driver.TextureFormat.RGBA16F, |
| "r32f": self.driver.TextureFormat.R32F, |
| "rgba32f": self.driver.TextureFormat.RGBA32F, |
| "bc1": self.driver.TextureFormat.BC1, |
| "bc3": self.driver.TextureFormat.BC3 |
| } |
| |
| filter_map = { |
| "nearest": self.driver.FilterMode.NEAREST, |
| "bilinear": self.driver.FilterMode.BILINEAR, |
| "trilinear": self.driver.FilterMode.TRILINEAR, |
| "anisotropic": self.driver.FilterMode.ANISOTROPIC |
| } |
| |
| wrap_map = { |
| "repeat": self.driver.WrapMode.REPEAT, |
| "clamp": self.driver.WrapMode.CLAMP, |
| "mirror": self.driver.WrapMode.MIRROR |
| } |
| |
| if format not in format_map: |
| raise ValueError(f"Invalid texture format: {format}") |
| if filter not in filter_map: |
| raise ValueError(f"Invalid filter mode: {filter}") |
| if wrap not in wrap_map: |
| raise ValueError(f"Invalid wrap mode: {wrap}") |
| |
| texture_id = self.driver.create_texture( |
| width=width, |
| height=height, |
| format=format_map[format], |
| filter_mode=filter_map[filter], |
| wrap_mode=wrap_map[wrap], |
| generate_mipmaps=generate_mipmaps, |
| aniso_level=aniso_level |
| ) |
| |
| print(f"Created texture with ID {texture_id}, format {format}, size {width}x{height}") |
| return texture_id |
| |
| def create_framebuffer(self, width, height, color_formats=None, |
| depth_format=None, stencil_format=None, |
| samples=1): |
| """ |
| Create a new framebuffer |
| |
| Args: |
| width, height: Framebuffer dimensions |
| color_formats: List of formats for color attachments |
| depth_format: Format for depth attachment (None to disable) |
| stencil_format: Format for stencil attachment (None to disable) |
| samples: MSAA sample count (1, 2, 4, or 8) |
| """ |
| |
| if color_formats is None: |
| color_formats = ["rgba8"] |
| |
| |
| format_map = { |
| "r8": self.driver.TextureFormat.R8, |
| "rgba8": self.driver.TextureFormat.RGBA8, |
| "rgba16f": self.driver.TextureFormat.RGBA16F, |
| "r32f": self.driver.TextureFormat.R32F |
| } |
| |
| samples_map = { |
| 1: self.driver.MSAASamples.MSAA_1X, |
| 2: self.driver.MSAASamples.MSAA_2X, |
| 4: self.driver.MSAASamples.MSAA_4X, |
| 8: self.driver.MSAASamples.MSAA_8X |
| } |
| |
| |
| color_format_enums = [] |
| for fmt in color_formats: |
| if fmt not in format_map: |
| raise ValueError(f"Invalid color format: {fmt}") |
| color_format_enums.append(format_map[fmt]) |
| |
| depth_format_enum = None |
| if depth_format: |
| if depth_format not in format_map: |
| raise ValueError(f"Invalid depth format: {depth_format}") |
| depth_format_enum = format_map[depth_format] |
| |
| stencil_format_enum = None |
| if stencil_format: |
| if stencil_format not in format_map: |
| raise ValueError(f"Invalid stencil format: {stencil_format}") |
| stencil_format_enum = format_map[stencil_format] |
| |
| if samples not in samples_map: |
| raise ValueError(f"Invalid sample count: {samples}") |
| |
| |
| fb_id = self.driver.create_framebuffer( |
| width=width, |
| height=height, |
| color_formats=color_format_enums, |
| depth_format=depth_format_enum, |
| stencil_format=stencil_format_enum, |
| samples=samples_map[samples] |
| ) |
| |
| |
| self.framebuffers[fb_id] = { |
| "width": width, |
| "height": height, |
| "color_formats": color_formats, |
| "depth_format": depth_format, |
| "stencil_format": stencil_format, |
| "samples": samples |
| } |
| |
| print(f"Created framebuffer with ID {fb_id}, size {width}x{height}, {samples}x MSAA") |
| return fb_id |
|
|
| def bind_framebuffer(self, fb_id): |
| """Bind framebuffer for rendering""" |
| if fb_id not in self.framebuffers: |
| raise ValueError(f"Invalid framebuffer ID: {fb_id}") |
| |
| self.current_framebuffer = fb_id |
| fb_info = self.framebuffers[fb_id] |
| print(f"Binding framebuffer {fb_id} ({fb_info['width']}x{fb_info['height']})") |
| |
| def bind_texture(self, texture_id, unit=0): |
| """Bind texture to a texture unit""" |
| if texture_id not in self.textures: |
| raise ValueError(f"Invalid texture ID: {texture_id}") |
| |
| self.current_texture_units[unit] = texture_id |
| print(f"Binding texture {texture_id} to unit {unit}") |
| |
| def set_viewport(self, x, y, width, height): |
| """Set viewport for rendering""" |
| if not self.current_framebuffer: |
| raise RuntimeError("No framebuffer bound") |
| |
| fb_info = self.framebuffers[self.current_framebuffer] |
| if x + width > fb_info['width'] or y + height > fb_info['height']: |
| raise ValueError("Viewport exceeds framebuffer dimensions") |
| |
| self.current_viewport = { |
| 'x': x, 'y': y, |
| 'width': width, 'height': height |
| } |
| |
| def clear(self, color=None, depth=None, stencil=None): |
| """Clear current framebuffer attachments""" |
| if not self.current_framebuffer: |
| raise RuntimeError("No framebuffer bound") |
| |
| fb_id = self.current_framebuffer |
| fb = self.framebuffers[fb_id] |
| |
| |
| driver_fb = self.driver.framebuffer_manager[fb_id] |
| |
| if color is not None: |
| r, g, b, a = color |
| driver_fb.set_clear_values(color=[r, g, b, a]) |
| |
| if depth is not None: |
| driver_fb.set_clear_values(depth=depth) |
| |
| if stencil is not None: |
| driver_fb.set_clear_values(stencil=stencil) |
| |
| |
| driver_fb.clear( |
| color=color is not None, |
| depth=depth is not None, |
| stencil=stencil is not None |
| ) |
| |
| print(f"Cleared framebuffer {fb_id}") |
| |
| def set_blend_state(self, enable=True, |
| src_factor="src_alpha", |
| dst_factor="one_minus_src_alpha", |
| blend_op="add"): |
| """Set blending state""" |
| if not self.current_framebuffer: |
| raise RuntimeError("No framebuffer bound") |
| |
| |
| factor_map = { |
| "zero": self.driver.BlendMode.ZERO, |
| "one": self.driver.BlendMode.ONE, |
| "src_color": self.driver.BlendMode.SRC_COLOR, |
| "one_minus_src_color": self.driver.BlendMode.ONE_MINUS_SRC_COLOR, |
| "dst_color": self.driver.BlendMode.DST_COLOR, |
| "one_minus_dst_color": self.driver.BlendMode.ONE_MINUS_DST_COLOR, |
| "src_alpha": self.driver.BlendMode.SRC_ALPHA, |
| "one_minus_src_alpha": self.driver.BlendMode.ONE_MINUS_SRC_ALPHA, |
| "dst_alpha": self.driver.BlendMode.DST_ALPHA, |
| "one_minus_dst_alpha": self.driver.BlendMode.ONE_MINUS_DST_ALPHA |
| } |
| |
| op_map = { |
| "add": self.driver.BlendOp.ADD, |
| "subtract": self.driver.BlendOp.SUBTRACT, |
| "reverse_subtract": self.driver.BlendOp.REVERSE_SUBTRACT, |
| "min": self.driver.BlendOp.MIN, |
| "max": self.driver.BlendOp.MAX |
| } |
| |
| if src_factor not in factor_map: |
| raise ValueError(f"Invalid source blend factor: {src_factor}") |
| if dst_factor not in factor_map: |
| raise ValueError(f"Invalid destination blend factor: {dst_factor}") |
| if blend_op not in op_map: |
| raise ValueError(f"Invalid blend operation: {blend_op}") |
| |
| fb_id = self.current_framebuffer |
| driver_fb = self.driver.framebuffer_manager[fb_id] |
| |
| |
| self.blend_state = { |
| 'enable': enable, |
| 'src_factor': factor_map[src_factor], |
| 'dst_factor': factor_map[dst_factor], |
| 'blend_op': op_map[blend_op] |
| } |
| |
| print(f"Set blend state: {enable}, {src_factor}, {dst_factor}, {blend_op}") |
|
|
|
|
|
|