// ======================================================================== // PART 5: PLAYDATE BACKEND IMPLEMENTATION (Level 3) // ======================================================================== // Same interface as SDL3, completely different implementation. // Shows where features degrade gracefully. function PlaydateBackend(pd) { this.pd = pd // Playdate API pointer this.target_pool = {} this.current_target = null this.current_camera = null } PlaydateBackend.prototype.get_capabilities = function() { return { supports_shaders: false, // NO SHADERS supports_render_targets: true, // LCDBitmap* supports_blend_modes: true, // Limited (copy, inverted, XOR, etc) supports_stencil: false, max_texture_size: 400, // Screen size basically native_mask_support: true, // setMask() is native! native_effects: ['dither', 'invert'] } } // ======================================================================== // RENDER TARGET MANAGEMENT // ======================================================================== PlaydateBackend.prototype.get_or_create_target = function(width, height, key) { var pool_key = `${width}x${height}` if (!this.target_pool[pool_key]) this.target_pool[pool_key] = [] // Reuse from pool arrfor(this.target_pool[pool_key], function(target) { if (!target.in_use) { target.in_use = true // Clear bitmap for reuse this.pd.graphics.pushContext(target.bitmap) this.pd.graphics.clear(this.pd.graphics.kColorClear) this.pd.graphics.popContext() return target } }) // Create new LCDBitmap var bitmap = this.pd.graphics.newBitmap(width, height, this.pd.graphics.kColorClear) var target = { bitmap: bitmap, width: width, height: height, in_use: true, key: key } this.target_pool[pool_key].push(target) return target } PlaydateBackend.prototype.release_all_targets = function() { arrfor(array(this.target_pool), function(key) { arrfor(this.target_pool[key], function(target) { target.in_use = false }) }) } // ======================================================================== // COMMAND EXECUTION // ======================================================================== PlaydateBackend.prototype.execute = function(commands) { // No command buffer concept - execute immediately arrfor(commands, function(cmd) { this.execute_command(cmd) }) this.release_all_targets() } PlaydateBackend.prototype.execute_command = function(cmd) { switch (cmd.cmd) { case 'begin_render': this.cmd_begin_render(cmd) break case 'end_render': this.cmd_end_render() break case 'set_camera': this.cmd_set_camera(cmd) break case 'draw_batch': this.cmd_draw_batch(cmd) break case 'shader_pass': this.cmd_shader_pass(cmd) // DEGRADES break case 'apply_mask': this.cmd_apply_mask(cmd) // NATIVE! break case 'composite': this.cmd_composite(cmd) break case 'blit': this.cmd_blit(cmd) break case 'clear': this.cmd_clear(cmd) break case 'present': // Nothing to do, display updates automatically break default: console.error(`Unknown command: ${cmd.cmd}`) } } PlaydateBackend.prototype.cmd_begin_render = function(cmd) { var target = cmd.target var clear = cmd.clear if (target == 'screen') { // Render to screen framebuffer this.current_target = 'screen' // Playdate: screen is always the current context unless you push } else { // Render to bitmap this.current_target = target this.pd.graphics.pushContext(target.bitmap) if (clear) { // Clear with color (Playdate is 1-bit, so color becomes pattern/dither) var pattern = this.color_to_dither_pattern(clear) this.pd.graphics.setDitherPattern(pattern) this.pd.graphics.fillRect(0, 0, target.width, target.height) } } } PlaydateBackend.prototype.cmd_end_render = function() { if (this.current_target != 'screen') { this.pd.graphics.popContext() } this.current_target = null } PlaydateBackend.prototype.cmd_set_camera = function(cmd) { this.current_camera = cmd.camera // Playdate doesn't have camera uniforms, we transform coordinates manually } PlaydateBackend.prototype.cmd_draw_batch = function(cmd) { var geometry = cmd.geometry var material = cmd.material || {} // Get image (Playdate uses LCDBitmap for textures) var image = this.get_image(material.texture || 'white') if (!image) return // Set draw mode based on material var draw_mode = this.material_to_draw_mode(material) this.pd.graphics.setImageDrawMode(draw_mode) // Draw each sprite in the batch for (var i = 0; i < length(geometry.indices); i += 6) { // Each sprite is 2 triangles = 6 indices = 4 vertices var vert_idx = geometry.indices[i] var v = geometry.vertices[vert_idx] // Transform by camera var screen_pos = this.world_to_screen(v.pos, this.current_camera) // Get sprite size from vertices var v2 = geometry.vertices[vert_idx + 2] var width = v2.pos[0] - v.pos[0] var height = v2.pos[1] - v.pos[1] // Transform size by camera zoom var scale = this.get_camera_scale(this.current_camera) width *= scale height *= scale // Apply vertex color as dither pattern (best we can do on 1-bit) if (v.color.a < 1.0) { var alpha_pattern = this.alpha_to_dither_pattern(v.color.a) this.pd.graphics.setDitherPattern(alpha_pattern) } // Draw image if (width != image.width || height != image.height) { // Need scaling var scaled = image.scaledImage(width / image.width, height / image.height) this.pd.graphics.drawBitmap(scaled, screen_pos[0], screen_pos[1]) } else { this.pd.graphics.drawBitmap(image, screen_pos[0], screen_pos[1]) } } } PlaydateBackend.prototype.cmd_shader_pass = function(cmd) { // NO SHADERS ON PLAYDATE // Degrade gracefully based on shader type var shader = cmd.shader var input = cmd.input var params = cmd.params if (shader == 'threshold') { // Threshold: Just copy input (or could dither based on threshold) console.warn('Threshold shader not supported on Playdate, copying') this.copy_bitmap(input.bitmap, this.current_target.bitmap) } else if (shader == 'gaussian_blur') { // Blur: Box blur in CPU (slow but possible) console.warn('Blur shader using CPU fallback') this.cpu_box_blur(input.bitmap, this.current_target.bitmap, params.radius || 5) } else if (shader == 'add_textures') { // Additive blend: Use XOR draw mode (not perfect but interesting) console.warn('Additive blend approximated with XOR') this.pd.graphics.setImageDrawMode(this.pd.graphics.kDrawModeXOR) this.pd.graphics.drawBitmap(input.bitmap, 0, 0) } else if (shader == 'crt_filter') { // CRT: Not possible, just copy console.warn('CRT filter not supported on Playdate, copying') this.copy_bitmap(input.bitmap, this.current_target.bitmap) } else { // Unknown shader: copy console.warn(`Shader ${shader} not supported on Playdate, copying`) this.copy_bitmap(input.bitmap, this.current_target.bitmap) } } PlaydateBackend.prototype.cmd_apply_mask = function(cmd) { // PLAYDATE HAS NATIVE MASK SUPPORT! var content = cmd.content_texture.bitmap var mask = cmd.mask_texture.bitmap var invert = cmd.invert if (invert) { // Invert mask first var inverted = this.pd.graphics.newBitmap(mask.width, mask.height) this.pd.graphics.pushContext(inverted) this.pd.graphics.setImageDrawMode(this.pd.graphics.kDrawModeInverted) this.pd.graphics.drawBitmap(mask, 0, 0) this.pd.graphics.popContext() mask = inverted } // Set mask on content bitmap content.setMask(mask) // Draw masked content to current target this.pd.graphics.drawBitmap(content, 0, 0) // Clear mask (don't leave it set) content.setMask(null) } PlaydateBackend.prototype.cmd_composite = function(cmd) { var base = cmd.base_texture.bitmap var overlay = cmd.overlay_texture.bitmap var mode = cmd.mode // Draw base this.pd.graphics.drawBitmap(base, 0, 0) // Draw overlay with appropriate mode var draw_mode = this.composite_mode_to_draw_mode(mode) this.pd.graphics.setImageDrawMode(draw_mode) this.pd.graphics.drawBitmap(overlay, 0, 0) // Reset draw mode this.pd.graphics.setImageDrawMode(this.pd.graphics.kDrawModeCopy) } PlaydateBackend.prototype.cmd_blit = function(cmd) { var bitmap = cmd.texture.bitmap var dst_rect = cmd.dst_rect var filter = cmd.filter // Scale bitmap to fit dst_rect var scale_x = dst_rect.width / bitmap.width var scale_y = dst_rect.height / bitmap.height if (scale_x != 1.0 || scale_y != 1.0) { // Playdate only supports nearest-neighbor scaling var scaled = bitmap.scaledImage(scale_x, scale_y) this.pd.graphics.drawBitmap(scaled, dst_rect.x, dst_rect.y) } else { this.pd.graphics.drawBitmap(bitmap, dst_rect.x, dst_rect.y) } } PlaydateBackend.prototype.cmd_clear = function(cmd) { var color = cmd.color var pattern = this.color_to_dither_pattern(color) this.pd.graphics.setDitherPattern(pattern) this.pd.graphics.fillRect(0, 0, 400, 240) // screen size } // ======================================================================== // HELPER FUNCTIONS // ======================================================================== PlaydateBackend.prototype.world_to_screen = function(world_pos, camera) { if (!camera) return world_pos // Simple orthographic transform var cam_x = camera.pos[0] var cam_y = camera.pos[1] var scale = this.get_camera_scale(camera) var screen_x = (world_pos[0] - cam_x) * scale + 200 // center X var screen_y = (world_pos[1] - cam_y) * scale + 120 // center Y return [screen_x, screen_y] } PlaydateBackend.prototype.get_camera_scale = function(camera) { if (!camera) return 1 // Scale based on camera width vs screen width return 400 / camera.width } PlaydateBackend.prototype.material_to_draw_mode = function(material) { var blend = material.blend || 'alpha' // Map blend modes to Playdate draw modes var mode_map = { 'alpha': this.pd.graphics.kDrawModeCopy, 'add': this.pd.graphics.kDrawModeXOR, // approximation 'multiply': this.pd.graphics.kDrawModeNXOR // approximation } return mode_map[blend] || this.pd.graphics.kDrawModeCopy } PlaydateBackend.prototype.composite_mode_to_draw_mode = function(mode) { var mode_map = { 'over': this.pd.graphics.kDrawModeCopy, 'add': this.pd.graphics.kDrawModeXOR, 'multiply': this.pd.graphics.kDrawModeNXOR } return mode_map[mode] || this.pd.graphics.kDrawModeCopy } PlaydateBackend.prototype.color_to_dither_pattern = function(color) { // Convert RGBA color to 8x8 dither pattern // Grayscale value determines density var gray = color.r * 0.299 + color.g * 0.587 + color.b * 0.114 return this.gray_to_dither_pattern(gray) } PlaydateBackend.prototype.gray_to_dither_pattern = function(gray) { // Bayer matrix dither patterns for different gray levels var patterns = [ // 0% (black) [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // ~25% [0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00], // ~50% [0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA], // ~75% [0x77, 0xFF, 0xDD, 0xFF, 0x77, 0xFF, 0xDD, 0xFF], // 100% (white) [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] ] var index = floor(gray * (length(patterns) - 1)) return patterns[index] } PlaydateBackend.prototype.alpha_to_dither_pattern = function(alpha) { return this.gray_to_dither_pattern(alpha) } PlaydateBackend.prototype.copy_bitmap = function(src, dst) { this.pd.graphics.pushContext(dst) this.pd.graphics.drawBitmap(src, 0, 0) this.pd.graphics.popContext() } PlaydateBackend.prototype.get_image = function(name) { // Look up image from asset system // Return LCDBitmap* return this.pd.graphics.imagetable(name)[1] } // ======================================================================== // CPU FALLBACKS FOR MISSING GPU FEATURES // ======================================================================== PlaydateBackend.prototype.cpu_box_blur = function(src, dst, radius) { // Simple box blur implementation in CPU // This is SLOW but correct var width = src.width var height = src.height // Get pixel data (Playdate API for accessing bitmap pixels) var src_data = src.getData() var dst_data = dst.getData() for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var sum = 0 var count = 0 // Sample neighborhood for (var dy = -radius; dy <= radius; dy++) { for (var dx = -radius; dx <= radius; dx++) { var sx = x + dx var sy = y + dy if (sx >= 0 && sx < width && sy >= 0 && sy < height) { sum += src_data[sy * width + sx] count++ } } } dst_data[y * width + x] = sum / count } } dst.setData(dst_data) }