NEWM / virtual_gpu /render.py
Factor Studios
Upload 167 files
684cc60 verified
"""
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!")