Spaces:
Runtime error
Runtime error
| """ | |
| Render Module - Software Raster Pipeline | |
| This module implements the software raster pipeline for drawing primitives | |
| and images onto framebuffers stored in VRAM. | |
| """ | |
| import numpy as np | |
| from typing import Tuple, Optional, Any, Dict | |
| import time | |
| class Renderer: | |
| """ | |
| Software-based renderer that implements basic drawing operations. | |
| This renderer operates on framebuffers stored in VRAM and provides | |
| functions for drawing primitives like rectangles, lines, and pixels. | |
| """ | |
| def __init__(self, vram=None): | |
| self.vram = vram | |
| self.current_shader = None | |
| # Rendering statistics | |
| self.pixels_drawn = 0 | |
| self.draw_calls = 0 | |
| self.render_time = 0.0 | |
| def set_vram(self, vram): | |
| """Set the VRAM reference.""" | |
| self.vram = vram | |
| def set_shader(self, shader): | |
| """Set the current shader for rendering operations.""" | |
| self.current_shader = shader | |
| def clear(self, framebuffer_id: str, color: Tuple[int, int, int] = (0, 0, 0)) -> bool: | |
| """Clear a framebuffer with the specified color.""" | |
| if not self.vram: | |
| return False | |
| start_time = time.time() | |
| framebuffer = self.vram.get_framebuffer(framebuffer_id) | |
| if not framebuffer: | |
| return False | |
| try: | |
| framebuffer.clear(color) | |
| self.pixels_drawn += framebuffer.width * framebuffer.height | |
| self.draw_calls += 1 | |
| self.render_time += time.time() - start_time | |
| return True | |
| except Exception as e: | |
| print(f"Error clearing framebuffer {framebuffer_id}: {e}") | |
| return False | |
| def draw_pixel(self, framebuffer_id: str, x: int, y: int, | |
| color: Tuple[int, int, int] = (255, 255, 255)) -> bool: | |
| """Draw a single pixel on the framebuffer.""" | |
| if not self.vram: | |
| return False | |
| start_time = time.time() | |
| framebuffer = self.vram.get_framebuffer(framebuffer_id) | |
| if not framebuffer: | |
| return False | |
| try: | |
| # Apply shader if available | |
| final_color = color | |
| if self.current_shader: | |
| final_color = self.current_shader.process_pixel(x, y, color) | |
| framebuffer.set_pixel(x, y, final_color) | |
| self.pixels_drawn += 1 | |
| self.draw_calls += 1 | |
| self.render_time += time.time() - start_time | |
| return True | |
| except Exception as e: | |
| print(f"Error drawing pixel at ({x}, {y}): {e}") | |
| return False | |
| def draw_rect(self, framebuffer_id: str, x: int, y: int, width: int, height: int, | |
| color: Tuple[int, int, int] = (255, 255, 255)) -> bool: | |
| """Draw a filled rectangle on the framebuffer.""" | |
| if not self.vram: | |
| return False | |
| start_time = time.time() | |
| framebuffer = self.vram.get_framebuffer(framebuffer_id) | |
| if not framebuffer: | |
| return False | |
| try: | |
| # Clamp rectangle to framebuffer bounds | |
| x1 = max(0, x) | |
| y1 = max(0, y) | |
| x2 = min(framebuffer.width, x + width) | |
| y2 = min(framebuffer.height, y + height) | |
| if x2 <= x1 or y2 <= y1: | |
| return True # Nothing to draw | |
| # Use NumPy for efficient rectangle filling | |
| if self.current_shader: | |
| # Apply shader to each pixel (slower but more flexible) | |
| for py in range(y1, y2): | |
| for px in range(x1, x2): | |
| final_color = self.current_shader.process_pixel(px, py, color) | |
| framebuffer.pixel_buffer[py, px] = final_color[:framebuffer.channels] | |
| else: | |
| # Direct fill (faster) | |
| framebuffer.pixel_buffer[y1:y2, x1:x2] = color[:framebuffer.channels] | |
| pixels_affected = (x2 - x1) * (y2 - y1) | |
| self.pixels_drawn += pixels_affected | |
| self.draw_calls += 1 | |
| self.render_time += time.time() - start_time | |
| return True | |
| except Exception as e: | |
| print(f"Error drawing rectangle at ({x}, {y}, {width}, {height}): {e}") | |
| return False | |
| def draw_line(self, framebuffer_id: str, x1: int, y1: int, x2: int, y2: int, | |
| color: Tuple[int, int, int] = (255, 255, 255)) -> bool: | |
| """Draw a line using Bresenham's algorithm.""" | |
| if not self.vram: | |
| return False | |
| start_time = time.time() | |
| framebuffer = self.vram.get_framebuffer(framebuffer_id) | |
| if not framebuffer: | |
| return False | |
| try: | |
| # Bresenham's line algorithm | |
| dx = abs(x2 - x1) | |
| dy = abs(y2 - y1) | |
| sx = 1 if x1 < x2 else -1 | |
| sy = 1 if y1 < y2 else -1 | |
| err = dx - dy | |
| x, y = x1, y1 | |
| pixels_drawn = 0 | |
| while True: | |
| # Draw pixel if within bounds | |
| if 0 <= x < framebuffer.width and 0 <= y < framebuffer.height: | |
| final_color = color | |
| if self.current_shader: | |
| final_color = self.current_shader.process_pixel(x, y, color) | |
| framebuffer.set_pixel(x, y, final_color) | |
| pixels_drawn += 1 | |
| if x == x2 and y == y2: | |
| break | |
| e2 = 2 * err | |
| if e2 > -dy: | |
| err -= dy | |
| x += sx | |
| if e2 < dx: | |
| err += dx | |
| y += sy | |
| self.pixels_drawn += pixels_drawn | |
| self.draw_calls += 1 | |
| self.render_time += time.time() - start_time | |
| return True | |
| except Exception as e: | |
| print(f"Error drawing line from ({x1}, {y1}) to ({x2}, {y2}): {e}") | |
| return False | |
| def draw_circle(self, framebuffer_id: str, center_x: int, center_y: int, radius: int, | |
| color: Tuple[int, int, int] = (255, 255, 255), filled: bool = False) -> bool: | |
| """Draw a circle using the midpoint circle algorithm.""" | |
| if not self.vram: | |
| return False | |
| start_time = time.time() | |
| framebuffer = self.vram.get_framebuffer(framebuffer_id) | |
| if not framebuffer: | |
| return False | |
| try: | |
| pixels_drawn = 0 | |
| if filled: | |
| # Draw filled circle | |
| for y in range(center_y - radius, center_y + radius + 1): | |
| for x in range(center_x - radius, center_x + radius + 1): | |
| if (x - center_x) ** 2 + (y - center_y) ** 2 <= radius ** 2: | |
| if 0 <= x < framebuffer.width and 0 <= y < framebuffer.height: | |
| final_color = color | |
| if self.current_shader: | |
| final_color = self.current_shader.process_pixel(x, y, color) | |
| framebuffer.set_pixel(x, y, final_color) | |
| pixels_drawn += 1 | |
| else: | |
| # Draw circle outline using midpoint algorithm | |
| x = 0 | |
| y = radius | |
| d = 1 - radius | |
| def draw_circle_points(cx, cy, x, y): | |
| points = [ | |
| (cx + x, cy + y), (cx - x, cy + y), | |
| (cx + x, cy - y), (cx - x, cy - y), | |
| (cx + y, cy + x), (cx - y, cy + x), | |
| (cx + y, cy - x), (cx - y, cy - x) | |
| ] | |
| drawn = 0 | |
| for px, py in points: | |
| if 0 <= px < framebuffer.width and 0 <= py < framebuffer.height: | |
| final_color = color | |
| if self.current_shader: | |
| final_color = self.current_shader.process_pixel(px, py, color) | |
| framebuffer.set_pixel(px, py, final_color) | |
| drawn += 1 | |
| return drawn | |
| pixels_drawn += draw_circle_points(center_x, center_y, x, y) | |
| while x < y: | |
| if d < 0: | |
| d += 2 * x + 3 | |
| else: | |
| d += 2 * (x - y) + 5 | |
| y -= 1 | |
| x += 1 | |
| pixels_drawn += draw_circle_points(center_x, center_y, x, y) | |
| self.pixels_drawn += pixels_drawn | |
| self.draw_calls += 1 | |
| self.render_time += time.time() - start_time | |
| return True | |
| except Exception as e: | |
| print(f"Error drawing circle at ({center_x}, {center_y}) with radius {radius}: {e}") | |
| return False | |
| def draw_image(self, framebuffer_id: str, x: int, y: int, texture_id: str, | |
| scale_x: float = 1.0, scale_y: float = 1.0) -> bool: | |
| """Draw an image/texture onto the framebuffer.""" | |
| if not self.vram: | |
| return False | |
| start_time = time.time() | |
| framebuffer = self.vram.get_framebuffer(framebuffer_id) | |
| texture = self.vram.get_texture(texture_id) | |
| if not framebuffer or texture is None: | |
| return False | |
| try: | |
| # Get texture dimensions | |
| if len(texture.shape) == 3: | |
| tex_height, tex_width, tex_channels = texture.shape | |
| else: | |
| tex_height, tex_width = texture.shape | |
| tex_channels = 1 | |
| # Calculate scaled dimensions | |
| scaled_width = int(tex_width * scale_x) | |
| scaled_height = int(tex_height * scale_y) | |
| pixels_drawn = 0 | |
| # Simple nearest-neighbor scaling and blitting | |
| for dy in range(scaled_height): | |
| for dx in range(scaled_width): | |
| # Calculate destination pixel | |
| dest_x = x + dx | |
| dest_y = y + dy | |
| # Check bounds | |
| if (dest_x < 0 or dest_x >= framebuffer.width or | |
| dest_y < 0 or dest_y >= framebuffer.height): | |
| continue | |
| # Calculate source pixel (nearest neighbor) | |
| src_x = int(dx / scale_x) | |
| src_y = int(dy / scale_y) | |
| # Clamp source coordinates | |
| src_x = min(src_x, tex_width - 1) | |
| src_y = min(src_y, tex_height - 1) | |
| # Get source pixel color | |
| if tex_channels == 1: | |
| color = (texture[src_y, src_x], texture[src_y, src_x], texture[src_y, src_x]) | |
| else: | |
| color = tuple(texture[src_y, src_x, :min(3, tex_channels)]) | |
| # Apply shader if available | |
| final_color = color | |
| if self.current_shader: | |
| final_color = self.current_shader.process_pixel(dest_x, dest_y, color) | |
| # Set pixel | |
| framebuffer.set_pixel(dest_x, dest_y, final_color) | |
| pixels_drawn += 1 | |
| self.pixels_drawn += pixels_drawn | |
| self.draw_calls += 1 | |
| self.render_time += time.time() - start_time | |
| return True | |
| except Exception as e: | |
| print(f"Error drawing image {texture_id} at ({x}, {y}): {e}") | |
| return False | |
| def get_stats(self) -> Dict[str, Any]: | |
| """Get rendering statistics.""" | |
| return { | |
| "pixels_drawn": self.pixels_drawn, | |
| "draw_calls": self.draw_calls, | |
| "total_render_time": self.render_time, | |
| "avg_render_time": self.render_time / max(1, self.draw_calls), | |
| "pixels_per_second": self.pixels_drawn / max(0.001, self.render_time) | |
| } | |
| def reset_stats(self) -> None: | |
| """Reset rendering statistics.""" | |
| self.pixels_drawn = 0 | |
| self.draw_calls = 0 | |
| self.render_time = 0.0 | |
| if __name__ == "__main__": | |
| # Test the renderer | |
| from vram import VRAM | |
| # Create VRAM and renderer | |
| vram = VRAM(memory_size_gb=1) | |
| renderer = Renderer(vram) | |
| # Create a test framebuffer | |
| fb_id = vram.create_framebuffer(800, 600, 3) | |
| # Test rendering operations | |
| print("Testing renderer...") | |
| # Clear screen | |
| renderer.clear(fb_id, (64, 128, 255)) | |
| # Draw some rectangles | |
| renderer.draw_rect(fb_id, 100, 100, 200, 150, (255, 0, 0)) | |
| renderer.draw_rect(fb_id, 200, 200, 100, 100, (0, 255, 0)) | |
| # Draw some lines | |
| renderer.draw_line(fb_id, 0, 0, 799, 599, (255, 255, 255)) | |
| renderer.draw_line(fb_id, 799, 0, 0, 599, (255, 255, 255)) | |
| # Draw a circle | |
| renderer.draw_circle(fb_id, 400, 300, 50, (255, 255, 0), filled=True) | |
| # Draw some pixels | |
| for i in range(100): | |
| renderer.draw_pixel(fb_id, 50 + i, 50, (255, 0, 255)) | |
| # Print statistics | |
| stats = renderer.get_stats() | |
| print(f"Renderer stats: {stats}") | |
| # Get framebuffer and check a pixel | |
| fb = vram.get_framebuffer(fb_id) | |
| if fb: | |
| pixel = fb.get_pixel(100, 100) | |
| print(f"Pixel at (100, 100): {pixel}") | |
| print("Renderer test completed!") | |