Implement CRT shader effect support
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			This commit is contained in:
		
							parent
							
								
									56342d9d8e
								
							
						
					
					
						commit
						67dee07f8e
					
				
							
								
								
									
										52
									
								
								debug/crt.frag
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								debug/crt.frag
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | |||||||
|  | #version 430 | ||||||
|  | 
 | ||||||
|  | layout (binding = 0) uniform sampler2D sprite; | ||||||
|  | 
 | ||||||
|  | layout (location = 0) in vec4 color; | ||||||
|  | layout (location = 1) in vec2 uv; | ||||||
|  | 
 | ||||||
|  | layout (location = 0) out vec4 texel; | ||||||
|  | 
 | ||||||
|  | layout (binding = 0) uniform Effect { | ||||||
|  | 	float screen_width; | ||||||
|  | 	float screen_height; | ||||||
|  | 	float time; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | vec3 scanline(vec2 coord, vec3 screen) | ||||||
|  | { | ||||||
|  | 	screen.rgb -= sin((coord.y + (time * 29.0))) * 0.02; | ||||||
|  | 	return screen; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | vec2 crt(vec2 coord, float bend) | ||||||
|  | { | ||||||
|  | 	// put in symmetrical coords | ||||||
|  | 	coord = (coord - 0.5) * 2.0; | ||||||
|  | 
 | ||||||
|  | 	coord *= 1.0; | ||||||
|  | 
 | ||||||
|  | 	// deform coords | ||||||
|  | 	coord.x *= 1.0 + pow((abs(coord.y) / bend), 2.0); | ||||||
|  | 	coord.y *= 1.0 + pow((abs(coord.x) / bend), 2.0); | ||||||
|  | 
 | ||||||
|  | 	// transform back to 0.0 - 1.0 space | ||||||
|  | 	coord  = (coord / 2.0) + 0.5; | ||||||
|  | 
 | ||||||
|  | 	return coord; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void main() | ||||||
|  | { | ||||||
|  | 	vec2 crtCoords = crt(uv, 4.8); | ||||||
|  | 
 | ||||||
|  | 	// Split the color channels | ||||||
|  | 	texel.rgb = texture(sprite, crtCoords).rgb; | ||||||
|  | 	texel.a = 1; | ||||||
|  | 
 | ||||||
|  | 	// HACK: this bend produces a shitty moire pattern. | ||||||
|  | 	// Up the bend for the scanline | ||||||
|  | 	vec2 screenSpace = crtCoords * vec2(screen_width, screen_height); | ||||||
|  | 	texel.rgb = scanline(screenSpace, texel.rgb); | ||||||
|  | } | ||||||
|  | 
 | ||||||
							
								
								
									
										52
									
								
								src/main.zig
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								src/main.zig
									
									
									
									
									
								
							| @ -9,11 +9,20 @@ const ChromaticAberration = extern struct { | |||||||
| 	padding: [12]u8 = undefined, | 	padding: [12]u8 = undefined, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const CRT = extern struct { | ||||||
|  | 	width: f32, | ||||||
|  | 	height: f32, | ||||||
|  | 	time: f32, | ||||||
|  | 	padding: [4]u8 = undefined, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| const Actors = struct { | const Actors = struct { | ||||||
| 	instances: coral.stack.Sequential(@Vector(2, f32)) = .{.allocator = coral.heap.allocator}, | 	instances: coral.stack.Sequential(@Vector(2, f32)) = .{.allocator = coral.heap.allocator}, | ||||||
| 	body_texture: ona.gfx.Texture = .default, | 	body_texture: ona.gfx.Texture = .default, | ||||||
| 	render_texture: ona.gfx.Texture = .default, | 	render_texture: ona.gfx.Texture = .default, | ||||||
| 	ca_effect: ona.gfx.Effect = .default, | 	ca_effect: ona.gfx.Effect = .default, | ||||||
|  | 	crt_effect: ona.gfx.Effect = .default, | ||||||
|  | 	staging_texture: ona.gfx.Texture = .default, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const Player = struct { | const Player = struct { | ||||||
| @ -44,6 +53,7 @@ fn load(config: ona.Write(ona.gfx.Config), actors: ona.Write(Actors), assets: on | |||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	actors.res.ca_effect = try assets.res.load_effect_file(coral.files.bundle, "./ca.frag.spv"); | 	actors.res.ca_effect = try assets.res.load_effect_file(coral.files.bundle, "./ca.frag.spv"); | ||||||
|  | 	actors.res.crt_effect = try assets.res.load_effect_file(coral.files.bundle, "./crt.frag.spv"); | ||||||
| 
 | 
 | ||||||
| 	try actors.res.instances.push_grow(.{0, 0}); | 	try actors.res.instances.push_grow(.{0, 0}); | ||||||
| } | } | ||||||
| @ -52,22 +62,46 @@ fn exit(actors: ona.Write(Actors)) void { | |||||||
| 	actors.res.instances.deinit(); | 	actors.res.instances.deinit(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn render(commands: ona.gfx.Commands, actors: ona.Write(Actors)) !void { | fn render(commands: ona.gfx.Commands, actors: ona.Write(Actors), app: ona.Read(ona.App)) !void { | ||||||
| 	try commands.set_effect(actors.res.ca_effect, ChromaticAberration{ | 	try commands.set_target(.{ | ||||||
| 		.effect_magnitude = 15.0, | 		.texture = actors.res.render_texture, | ||||||
|  | 		.clear_color = ona.gfx.colors.black, | ||||||
|  | 		.clear_depth = 0, | ||||||
|  | 		.clear_stencil = 0, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	for (actors.res.instances.values) |instance| { |  | ||||||
| 	try commands.draw_texture(.{ | 	try commands.draw_texture(.{ | ||||||
| 			.texture = actors.res.body_texture, | 		.texture = .default, | ||||||
| 
 | 
 | ||||||
| 		.transform = .{ | 		.transform = .{ | ||||||
| 				.origin = instance, | 			.origin = .{1280 / 2, 720 / 2}, | ||||||
| 				.xbasis = .{64, 0}, | 			.xbasis = .{1280, 0}, | ||||||
| 				.ybasis = .{0, 64}, | 			.ybasis = .{0, 720}, | ||||||
|  | 		}, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	try commands.set_effect(actors.res.crt_effect, CRT{ | ||||||
|  | 		.width = 1280, | ||||||
|  | 		.height = 720, | ||||||
|  | 		.time = @floatCast(app.res.elapsed_time), | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	try commands.set_target(.{ | ||||||
|  | 		.texture = .backbuffer, | ||||||
|  | 		.clear_color = null, | ||||||
|  | 		.clear_depth = null, | ||||||
|  | 		.clear_stencil = null, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	try commands.draw_texture(.{ | ||||||
|  | 		.texture = actors.res.render_texture, | ||||||
|  | 
 | ||||||
|  | 		.transform = .{ | ||||||
|  | 			.origin = .{1280 / 2, 720 / 2}, | ||||||
|  | 			.xbasis = .{1280, 0}, | ||||||
|  | 			.ybasis = .{0, 720}, | ||||||
| 		}, | 		}, | ||||||
| 	}); | 	}); | ||||||
| 	} |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn update(player: ona.Read(Player), actors: ona.Write(Actors), mapping: ona.Read(ona.act.Mapping)) !void { | fn update(player: ona.Read(Player), actors: ona.Write(Actors), mapping: ona.Read(ona.act.Mapping)) !void { | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ const flow = @import("flow"); | |||||||
| 
 | 
 | ||||||
| events: *const Events, | events: *const Events, | ||||||
| target_frame_time: f64, | target_frame_time: f64, | ||||||
|  | elapsed_time: f64, | ||||||
| is_running: bool, | is_running: bool, | ||||||
| 
 | 
 | ||||||
| pub const Events = struct { | pub const Events = struct { | ||||||
|  | |||||||
| @ -17,7 +17,12 @@ fn Handle(comptime HandleDesc: type) type { | |||||||
| 	}; | 	}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub const Texture = Handle(struct { | pub const Texture = enum (u32) { | ||||||
|  | 	default, | ||||||
|  | 	backbuffer, | ||||||
|  | 	_, | ||||||
|  | 
 | ||||||
|  | 	pub const Desc = struct { | ||||||
| 		format: Format, | 		format: Format, | ||||||
| 		access: Access, | 		access: Access, | ||||||
| 
 | 
 | ||||||
| @ -46,4 +51,6 @@ pub const Texture = Handle(struct { | |||||||
| 				}; | 				}; | ||||||
| 			} | 			} | ||||||
| 		}; | 		}; | ||||||
| }); | 	}; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ pub const Command = union (enum) { | |||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	pub const SetTarget = struct { | 	pub const SetTarget = struct { | ||||||
| 		texture: ?handles.Texture = null, | 		texture: handles.Texture, | ||||||
| 		clear_color: ?lina.Color, | 		clear_color: ?lina.Color, | ||||||
| 		clear_depth: ?f32, | 		clear_depth: ?f32, | ||||||
| 		clear_stencil: ?u8, | 		clear_stencil: ?u8, | ||||||
| @ -135,7 +135,7 @@ const Frame = struct { | |||||||
| 	drawn_count: usize = 0, | 	drawn_count: usize = 0, | ||||||
| 	flushed_count: usize = 0, | 	flushed_count: usize = 0, | ||||||
| 	current_source_texture: handles.Texture = .default, | 	current_source_texture: handles.Texture = .default, | ||||||
| 	current_target_texture: ?handles.Texture = null, | 	current_target_texture: handles.Texture = .backbuffer, | ||||||
| 	current_effect: handles.Effect = .default, | 	current_effect: handles.Effect = .default, | ||||||
| 
 | 
 | ||||||
| 	const DrawTexture = extern struct { | 	const DrawTexture = extern struct { | ||||||
| @ -185,24 +185,27 @@ const Frame = struct { | |||||||
| 
 | 
 | ||||||
| 		bindings.vertex_buffers[vertex_indices.mesh] = quad_vertex_buffer; | 		bindings.vertex_buffers[vertex_indices.mesh] = quad_vertex_buffer; | ||||||
| 
 | 
 | ||||||
| 		switch (pools.textures.get(@intFromEnum(self.current_source_texture)).?.access) { | 		switch (pools.get_texture(self.current_source_texture).?.access) { | ||||||
| 			.render => |render| { | 			.render => |render| { | ||||||
| 				bindings.fs.images[0] = render.color_image; | 				bindings.fs.images[0] = render.color_image; | ||||||
| 				bindings.fs.samplers[0] = render.sampler; | 				bindings.fs.samplers[0] = default_sampler; | ||||||
| 			}, | 			}, | ||||||
| 
 | 
 | ||||||
| 			.static => |static| { | 			.static => |static| { | ||||||
| 				bindings.fs.images[0] = static.image; | 				bindings.fs.images[0] = static.image; | ||||||
| 				bindings.fs.samplers[0] = static.sampler; | 				bindings.fs.samplers[0] = default_sampler; | ||||||
|  | 			}, | ||||||
|  | 
 | ||||||
|  | 			.empty => { | ||||||
|  | 				@panic("Cannot render empty textures"); | ||||||
| 			}, | 			}, | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		const effect = pools.effects.get(@intFromEnum(self.current_effect)).?; | 		const effect = pools.get_effect(self.current_effect).?; | ||||||
| 
 | 
 | ||||||
| 		sokol.gfx.applyPipeline(effect.pipeline); | 		sokol.gfx.applyPipeline(effect.pipeline); | ||||||
| 
 | 
 | ||||||
| 		if (self.current_target_texture) |target_texture| { | 		const texture = pools.get_texture(self.current_target_texture).?; | ||||||
| 			const texture = pools.textures.get(@intFromEnum(target_texture)).?; |  | ||||||
| 
 | 
 | ||||||
| 		sokol.gfx.applyUniforms(.VS, 0, sokol.gfx.asRange(&lina.orthographic_projection(-1.0, 1.0, .{ | 		sokol.gfx.applyUniforms(.VS, 0, sokol.gfx.asRange(&lina.orthographic_projection(-1.0, 1.0, .{ | ||||||
| 			.left = 0, | 			.left = 0, | ||||||
| @ -210,16 +213,10 @@ const Frame = struct { | |||||||
| 			.right = @floatFromInt(texture.width), | 			.right = @floatFromInt(texture.width), | ||||||
| 			.bottom = @floatFromInt(texture.height), | 			.bottom = @floatFromInt(texture.height), | ||||||
| 		}))); | 		}))); | ||||||
| 		} else { |  | ||||||
| 			sokol.gfx.applyUniforms(.VS, 0, sokol.gfx.asRange(&lina.orthographic_projection(-1.0, 1.0, .{ |  | ||||||
| 				.left = 0, |  | ||||||
| 				.top = 0, |  | ||||||
| 				.right = @floatFromInt(self.swapchain.width), |  | ||||||
| 				.bottom = @floatFromInt(self.swapchain.height), |  | ||||||
| 			}))); |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
|  | 		if (effect.properties.len != 0) { | ||||||
| 			sokol.gfx.applyUniforms(.FS, 0, sokol.gfx.asRange(effect.properties)); | 			sokol.gfx.applyUniforms(.FS, 0, sokol.gfx.asRange(effect.properties)); | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 		while (true) { | 		while (true) { | ||||||
| 			const buffer_index = self.flushed_count / batches_per_buffer; | 			const buffer_index = self.flushed_count / batches_per_buffer; | ||||||
| @ -246,7 +243,7 @@ const Frame = struct { | |||||||
| 
 | 
 | ||||||
| 		self.current_effect = command.effect; | 		self.current_effect = command.effect; | ||||||
| 
 | 
 | ||||||
| 		if (pools.effects.get(@intFromEnum(self.current_effect))) |effect| { | 		if (pools.get_effect(self.current_effect)) |effect| { | ||||||
| 			@memcpy(effect.properties, command.properties); | 			@memcpy(effect.properties, command.properties); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @ -280,17 +277,13 @@ const Frame = struct { | |||||||
| 			pass.action.depth = .{.load_action = .LOAD}; | 			pass.action.depth = .{.load_action = .LOAD}; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if (command.texture) |texture| { | 		pass.attachments = switch (pools.get_texture(self.current_target_texture).?.access) { | ||||||
| 			pass.attachments = switch (pools.textures.get(@intFromEnum(texture)).?.access) { |  | ||||||
| 			.static => @panic("Cannot render to static textures"), | 			.static => @panic("Cannot render to static textures"), | ||||||
|  | 			.empty => @panic("Cannot render to empty textures"), | ||||||
| 			.render => |render| render.attachments, | 			.render => |render| render.attachments, | ||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
| 		self.current_target_texture = command.texture; | 		self.current_target_texture = command.texture; | ||||||
| 		} else { |  | ||||||
| 			pass.swapchain = self.swapchain; |  | ||||||
| 			self.current_target_texture = null; |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		sokol.gfx.beginPass(pass); | 		sokol.gfx.beginPass(pass); | ||||||
| 	} | 	} | ||||||
| @ -313,7 +306,7 @@ const Pools = struct { | |||||||
| 
 | 
 | ||||||
| 	const TexturePool = coral.Pool(resources.Texture); | 	const TexturePool = coral.Pool(resources.Texture); | ||||||
| 
 | 
 | ||||||
| 	fn create_effect(self: *Pools, desc: handles.Effect.Desc) !handles.Effect { | 	pub fn create_effect(self: *Pools, desc: handles.Effect.Desc) !handles.Effect { | ||||||
| 		var effect = try resources.Effect.init(desc); | 		var effect = try resources.Effect.init(desc); | ||||||
| 
 | 
 | ||||||
| 		errdefer effect.deinit(); | 		errdefer effect.deinit(); | ||||||
| @ -321,7 +314,7 @@ const Pools = struct { | |||||||
| 		return @enumFromInt(try self.effects.insert(effect)); | 		return @enumFromInt(try self.effects.insert(effect)); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fn create_texture(self: *Pools, desc: handles.Texture.Desc) !handles.Texture { | 	pub fn create_texture(self: *Pools, desc: handles.Texture.Desc) !handles.Texture { | ||||||
| 		var texture = try resources.Texture.init(desc); | 		var texture = try resources.Texture.init(desc); | ||||||
| 
 | 
 | ||||||
| 		errdefer texture.deinit(); | 		errdefer texture.deinit(); | ||||||
| @ -329,7 +322,7 @@ const Pools = struct { | |||||||
| 		return @enumFromInt(try self.textures.insert(texture)); | 		return @enumFromInt(try self.textures.insert(texture)); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fn deinit(self: *Pools) void { | 	pub fn deinit(self: *Pools) void { | ||||||
| 		var textures = self.textures.values(); | 		var textures = self.textures.values(); | ||||||
| 
 | 
 | ||||||
| 		while (textures.next()) |texture| { | 		while (textures.next()) |texture| { | ||||||
| @ -339,7 +332,7 @@ const Pools = struct { | |||||||
| 		self.textures.deinit(); | 		self.textures.deinit(); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fn destroy_effect(self: *Pools, handle: handles.Effect) bool { | 	pub fn destroy_effect(self: *Pools, handle: handles.Effect) bool { | ||||||
| 		switch (handle) { | 		switch (handle) { | ||||||
| 			.default => {}, | 			.default => {}, | ||||||
| 
 | 
 | ||||||
| @ -355,7 +348,7 @@ const Pools = struct { | |||||||
| 		return true; | 		return true; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fn destroy_texture(self: *Pools, handle: handles.Texture) bool { | 	pub fn destroy_texture(self: *Pools, handle: handles.Texture) bool { | ||||||
| 		switch (handle) { | 		switch (handle) { | ||||||
| 			.default => {}, | 			.default => {}, | ||||||
| 
 | 
 | ||||||
| @ -371,7 +364,15 @@ const Pools = struct { | |||||||
| 		return true; | 		return true; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fn init(allocator: std.mem.Allocator) !Pools { | 	pub fn get_effect(self: *Pools, handle: handles.Effect) ?*resources.Effect { | ||||||
|  | 		return self.effects.get(@intFromEnum(handle)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	pub fn get_texture(self: *Pools, handle: handles.Texture) ?*resources.Texture { | ||||||
|  | 		return self.textures.get(@intFromEnum(handle)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	pub fn init(allocator: std.mem.Allocator) !Pools { | ||||||
| 		var pools = Pools{ | 		var pools = Pools{ | ||||||
| 			.effects = EffectPool.init(allocator), | 			.effects = EffectPool.init(allocator), | ||||||
| 			.textures = TexturePool.init(allocator), | 			.textures = TexturePool.init(allocator), | ||||||
| @ -381,31 +382,47 @@ const Pools = struct { | |||||||
| 			pools.deinit(); | 			pools.deinit(); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		_ = try pools.create_effect(.{ | 		const assert = struct { | ||||||
| 			.fragment_spirv_ops = &spirv.to_ops(@embedFile("./shaders/2d_default.frag.spv")), | 			fn is_handle(expected: anytype, actual: @TypeOf(expected)) void { | ||||||
| 		}); | 				std.debug.assert(actual == expected); | ||||||
| 
 | 			} | ||||||
| 		const default_texture_data = [_]u32{ |  | ||||||
| 			0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, |  | ||||||
| 			0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, |  | ||||||
| 			0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, |  | ||||||
| 			0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, |  | ||||||
| 			0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, |  | ||||||
| 			0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, |  | ||||||
| 			0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, |  | ||||||
| 			0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, |  | ||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
| 		_ = try pools.create_texture(.{ | 		assert.is_handle(handles.Effect.default, try pools.create_effect(.{ | ||||||
|  | 			.fragment_spirv_ops = &spirv.to_ops(@embedFile("./shaders/2d_default.frag.spv")), | ||||||
|  | 		})); | ||||||
|  | 
 | ||||||
|  | 		assert.is_handle(handles.Texture.default, try pools.create_texture(.{ | ||||||
| 			.format = .rgba8, | 			.format = .rgba8, | ||||||
| 
 | 
 | ||||||
| 			.access = .{ | 			.access = .{ | ||||||
| 				.static = .{ | 				.static = .{ | ||||||
| 					.data = std.mem.asBytes(&default_texture_data), | 					.data = std.mem.asBytes(&[_]u32{ | ||||||
|  | 						0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, | ||||||
|  | 						0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, | ||||||
|  | 						0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, | ||||||
|  | 						0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, | ||||||
|  | 						0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, | ||||||
|  | 						0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, | ||||||
|  | 						0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, | ||||||
|  | 						0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, | ||||||
|  | 					}), | ||||||
|  | 
 | ||||||
| 					.width = 8, | 					.width = 8, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 		}); | 		})); | ||||||
|  | 
 | ||||||
|  | 		assert.is_handle(handles.Texture.backbuffer, try pools.create_texture(.{ | ||||||
|  | 			.format = .rgba8, | ||||||
|  | 
 | ||||||
|  | 			.access = .{ | ||||||
|  | 				.render = .{ | ||||||
|  | 					.width = 0, | ||||||
|  | 					.height = 0, | ||||||
|  | 				}, | ||||||
|  | 			} | ||||||
|  | 		})); | ||||||
| 
 | 
 | ||||||
| 		return pools; | 		return pools; | ||||||
| 	} | 	} | ||||||
| @ -457,6 +474,8 @@ pub const Work = union (enum) { | |||||||
| 	var pending: coral.asyncio.BlockingQueue(1024, Work) = .{}; | 	var pending: coral.asyncio.BlockingQueue(1024, Work) = .{}; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | var default_sampler: sokol.gfx.Sampler = undefined; | ||||||
|  | 
 | ||||||
| pub fn enqueue_work(work: Work) void { | pub fn enqueue_work(work: Work) void { | ||||||
| 	Work.pending.enqueue(work); | 	Work.pending.enqueue(work); | ||||||
| } | } | ||||||
| @ -541,6 +560,8 @@ fn run(window: *ext.SDL_Window) !void { | |||||||
| 		.type = .VERTEXBUFFER, | 		.type = .VERTEXBUFFER, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
|  | 	default_sampler = sokol.gfx.makeSampler(.{}); | ||||||
|  | 
 | ||||||
| 	var has_commands_head: ?*Commands = null; | 	var has_commands_head: ?*Commands = null; | ||||||
| 	var has_commands_tail: ?*Commands = null; | 	var has_commands_tail: ?*Commands = null; | ||||||
| 
 | 
 | ||||||
| @ -588,6 +609,23 @@ fn run(window: *ext.SDL_Window) !void { | |||||||
| 			}, | 			}, | ||||||
| 
 | 
 | ||||||
| 			.render_frame => |render_frame| { | 			.render_frame => |render_frame| { | ||||||
|  | 				const backbuffer = pools.get_texture(.backbuffer).?; | ||||||
|  | 
 | ||||||
|  | 				if (backbuffer.width != render_frame.width or backbuffer.height != render_frame.height) { | ||||||
|  | 					backbuffer.deinit(); | ||||||
|  | 
 | ||||||
|  | 					backbuffer.* = try resources.Texture.init(.{ | ||||||
|  | 						.format = .rgba8, | ||||||
|  | 
 | ||||||
|  | 						.access = .{ | ||||||
|  | 							.render = .{ | ||||||
|  | 								.width = render_frame.width, | ||||||
|  | 								.height = render_frame.height, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
| 				var frame = Frame{ | 				var frame = Frame{ | ||||||
| 					.swapchain = .{ | 					.swapchain = .{ | ||||||
| 						.width = render_frame.width, | 						.width = render_frame.width, | ||||||
| @ -599,6 +637,51 @@ fn run(window: *ext.SDL_Window) !void { | |||||||
| 					} | 					} | ||||||
| 				}; | 				}; | ||||||
| 
 | 
 | ||||||
|  | 				sokol.gfx.beginPass(pass: { | ||||||
|  | 					var pass = sokol.gfx.Pass{ | ||||||
|  | 						.action = .{ | ||||||
|  | 							.stencil = .{ | ||||||
|  | 								.load_action = .CLEAR, | ||||||
|  | 							}, | ||||||
|  | 
 | ||||||
|  | 							.depth = .{ | ||||||
|  | 								.load_action = .CLEAR, | ||||||
|  | 								.clear_value = 0, | ||||||
|  | 							} | ||||||
|  | 						}, | ||||||
|  | 					}; | ||||||
|  | 
 | ||||||
|  | 					pass.action.colors[0] = .{ | ||||||
|  | 						.load_action = .CLEAR, | ||||||
|  | 						.clear_value = @bitCast(render_frame.clear_color), | ||||||
|  | 					}; | ||||||
|  | 
 | ||||||
|  | 					pass.attachments = pools.get_texture(.backbuffer).?.access.render.attachments; | ||||||
|  | 
 | ||||||
|  | 					break: pass pass; | ||||||
|  | 				}); | ||||||
|  | 
 | ||||||
|  | 				var has_commands = has_commands_head; | ||||||
|  | 
 | ||||||
|  | 				while (has_commands) |commands| : (has_commands = commands.has_next) { | ||||||
|  | 					for (commands.submitted_commands()) |command| { | ||||||
|  | 						try command.process(&pools, &frame); | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					frame.flush(&pools); | ||||||
|  | 
 | ||||||
|  | 					if (frame.current_target_texture != .backbuffer) { | ||||||
|  | 						frame.set_target(&pools, .{ | ||||||
|  | 							.texture = .backbuffer, | ||||||
|  | 							.clear_color = null, | ||||||
|  | 							.clear_depth = null, | ||||||
|  | 							.clear_stencil = null, | ||||||
|  | 						}); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				sokol.gfx.endPass(); | ||||||
|  | 
 | ||||||
| 				sokol.gfx.beginPass(swapchain_pass: { | 				sokol.gfx.beginPass(swapchain_pass: { | ||||||
| 					var pass = sokol.gfx.Pass{ | 					var pass = sokol.gfx.Pass{ | ||||||
| 						.swapchain = frame.swapchain, | 						.swapchain = frame.swapchain, | ||||||
| @ -609,29 +692,20 @@ fn run(window: *ext.SDL_Window) !void { | |||||||
| 						}, | 						}, | ||||||
| 					}; | 					}; | ||||||
| 
 | 
 | ||||||
| 					pass.action.colors[0] = .{ | 					pass.action.colors[0] = .{.load_action = .CLEAR}; | ||||||
| 						.clear_value = @bitCast(render_frame.clear_color), |  | ||||||
| 						.load_action = .CLEAR, |  | ||||||
| 					}; |  | ||||||
| 
 | 
 | ||||||
| 					break: swapchain_pass pass; | 					break: swapchain_pass pass; | ||||||
| 				}); | 				}); | ||||||
| 
 | 
 | ||||||
| 				var has_commands = has_commands_head; | 				try frame.draw_texture(&pools, .{ | ||||||
|  | 					.texture = .backbuffer, | ||||||
| 
 | 
 | ||||||
| 				while (has_commands) |commands| : (has_commands = commands.has_next) { | 					.transform = .{ | ||||||
| 					for (commands.submitted_commands()) |command| { | 						.origin = .{@as(f32, @floatFromInt(render_frame.width)) / 2, @as(f32, @floatFromInt(render_frame.height)) / 2}, | ||||||
| 						try command.process(&pools, &frame); | 						.xbasis = .{@floatFromInt(render_frame.width), 0}, | ||||||
| 					} | 						.ybasis = .{0, @floatFromInt(render_frame.height)}, | ||||||
| 
 | 					}, | ||||||
| 					if (frame.current_target_texture != .default) { |  | ||||||
| 						frame.set_target(&pools, .{ |  | ||||||
| 							.clear_color = null, |  | ||||||
| 							.clear_depth = null, |  | ||||||
| 							.clear_stencil = null, |  | ||||||
| 				}); | 				}); | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 | 
 | ||||||
| 				frame.flush(&pools); | 				frame.flush(&pools); | ||||||
| 				sokol.gfx.endPass(); | 				sokol.gfx.endPass(); | ||||||
|  | |||||||
| @ -123,11 +123,11 @@ pub const Effect = struct { | |||||||
| 						.float2 => .FLOAT2, | 						.float2 => .FLOAT2, | ||||||
| 						.float3 => .FLOAT3, | 						.float3 => .FLOAT3, | ||||||
| 						.float4 => .FLOAT4, | 						.float4 => .FLOAT4, | ||||||
| 						.int => .INT, | 						.integer => .INT, | ||||||
| 						.int2 => .INT2, | 						.integer2 => .INT2, | ||||||
| 						.int3 => .INT3, | 						.integer3 => .INT3, | ||||||
| 						.int4 => .INT4, | 						.integer4 => .INT4, | ||||||
| 						.mat4 => .MAT4, | 						.matrix4 => .MAT4, | ||||||
| 					}, | 					}, | ||||||
| 
 | 
 | ||||||
| 					.name = uniform.name, | 					.name = uniform.name, | ||||||
| @ -244,19 +244,18 @@ pub const Texture = struct { | |||||||
| 	access: Access, | 	access: Access, | ||||||
| 
 | 
 | ||||||
| 	pub const Access = union (enum) { | 	pub const Access = union (enum) { | ||||||
|  | 		empty, | ||||||
| 		render: RenderAccess, | 		render: RenderAccess, | ||||||
| 		static: StaticAccess, | 		static: StaticAccess, | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	pub const RenderAccess = struct { | 	pub const RenderAccess = struct { | ||||||
| 		sampler: sokol.gfx.Sampler, |  | ||||||
| 		color_image: sokol.gfx.Image, | 		color_image: sokol.gfx.Image, | ||||||
| 		depth_image: sokol.gfx.Image, | 		depth_image: sokol.gfx.Image, | ||||||
| 		attachments: sokol.gfx.Attachments, | 		attachments: sokol.gfx.Attachments, | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	pub const StaticAccess = struct { | 	pub const StaticAccess = struct { | ||||||
| 		sampler: sokol.gfx.Sampler, |  | ||||||
| 		image: sokol.gfx.Image, | 		image: sokol.gfx.Image, | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| @ -265,14 +264,14 @@ pub const Texture = struct { | |||||||
| 			.render => |render| { | 			.render => |render| { | ||||||
| 				sokol.gfx.destroyImage(render.color_image); | 				sokol.gfx.destroyImage(render.color_image); | ||||||
| 				sokol.gfx.destroyImage(render.depth_image); | 				sokol.gfx.destroyImage(render.depth_image); | ||||||
| 				sokol.gfx.destroySampler(render.sampler); |  | ||||||
| 				sokol.gfx.destroyAttachments(render.attachments); | 				sokol.gfx.destroyAttachments(render.attachments); | ||||||
| 			}, | 			}, | ||||||
| 
 | 
 | ||||||
| 			.static => |static| { | 			.static => |static| { | ||||||
| 				sokol.gfx.destroyImage(static.image); | 				sokol.gfx.destroyImage(static.image); | ||||||
| 				sokol.gfx.destroySampler(static.sampler); |  | ||||||
| 			}, | 			}, | ||||||
|  | 
 | ||||||
|  | 			.empty => {}, | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		self.* = undefined; | 		self.* = undefined; | ||||||
| @ -286,6 +285,14 @@ pub const Texture = struct { | |||||||
| 
 | 
 | ||||||
| 		switch (desc.access) { | 		switch (desc.access) { | ||||||
| 			.render => |render| { | 			.render => |render| { | ||||||
|  | 				if (render.width == 0 or render.height == 0) { | ||||||
|  | 					return .{ | ||||||
|  | 						.width = render.width, | ||||||
|  | 						.height = render.height, | ||||||
|  | 						.access = .empty, | ||||||
|  | 					}; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
| 				const color_image = sokol.gfx.makeImage(.{ | 				const color_image = sokol.gfx.makeImage(.{ | ||||||
| 					.pixel_format = pixel_format, | 					.pixel_format = pixel_format, | ||||||
| 					.width = render.width, | 					.width = render.width, | ||||||
| @ -314,8 +321,6 @@ pub const Texture = struct { | |||||||
| 					break: attachments_desc attachments_desc; | 					break: attachments_desc attachments_desc; | ||||||
| 				}); | 				}); | ||||||
| 
 | 
 | ||||||
| 				const sampler = sokol.gfx.makeSampler(.{}); |  | ||||||
| 
 |  | ||||||
| 				return .{ | 				return .{ | ||||||
| 					.width = render.width, | 					.width = render.width, | ||||||
| 					.height = render.height, | 					.height = render.height, | ||||||
| @ -323,11 +328,10 @@ pub const Texture = struct { | |||||||
| 					.access = .{ | 					.access = .{ | ||||||
| 						.render = .{ | 						.render = .{ | ||||||
| 							.attachments = attachments, | 							.attachments = attachments, | ||||||
| 							.sampler = sampler, |  | ||||||
| 							.color_image = color_image, | 							.color_image = color_image, | ||||||
| 							.depth_image = depth_image, | 							.depth_image = depth_image, | ||||||
| 						}, | 						}, | ||||||
| 					} | 					}, | ||||||
| 				}; | 				}; | ||||||
| 			}, | 			}, | ||||||
| 
 | 
 | ||||||
| @ -336,6 +340,14 @@ pub const Texture = struct { | |||||||
| 					return error.OutOfMemory; | 					return error.OutOfMemory; | ||||||
| 				}; | 				}; | ||||||
| 
 | 
 | ||||||
|  | 				if (static.width == 0 or height == 0) { | ||||||
|  | 					return .{ | ||||||
|  | 						.width = static.width, | ||||||
|  | 						.height = height, | ||||||
|  | 						.access = .empty, | ||||||
|  | 					}; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
| 				const image = sokol.gfx.makeImage(image_desc: { | 				const image = sokol.gfx.makeImage(image_desc: { | ||||||
| 					var image_desc = sokol.gfx.ImageDesc{ | 					var image_desc = sokol.gfx.ImageDesc{ | ||||||
| 						.height = height, | 						.height = height, | ||||||
| @ -348,10 +360,7 @@ pub const Texture = struct { | |||||||
| 					break: image_desc image_desc; | 					break: image_desc image_desc; | ||||||
| 				}); | 				}); | ||||||
| 
 | 
 | ||||||
| 				const sampler = sokol.gfx.makeSampler(.{}); |  | ||||||
| 
 |  | ||||||
| 				errdefer { | 				errdefer { | ||||||
| 					sokol.gfx.destroySampler(sampler); |  | ||||||
| 					sokol.gfx.destroyImage(image); | 					sokol.gfx.destroyImage(image); | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| @ -362,7 +371,6 @@ pub const Texture = struct { | |||||||
| 					.access = .{ | 					.access = .{ | ||||||
| 						.static = .{ | 						.static = .{ | ||||||
| 							.image = image, | 							.image = image, | ||||||
| 							.sampler = sampler, |  | ||||||
| 						}, | 						}, | ||||||
| 					}, | 					}, | ||||||
| 				}; | 				}; | ||||||
|  | |||||||
| @ -21,8 +21,9 @@ void main() { | |||||||
| 	const vec2 world_position = instance_origin + mesh_xy.x * instance_xbasis + mesh_xy.y * instance_ybasis; | 	const vec2 world_position = instance_origin + mesh_xy.x * instance_xbasis + mesh_xy.y * instance_ybasis; | ||||||
| 	const vec2 projected_position = (projection_matrix * vec4(world_position, 0, 1)).xy; | 	const vec2 projected_position = (projection_matrix * vec4(world_position, 0, 1)).xy; | ||||||
| 	const vec2 rect_size = instance_rect.zw - instance_rect.xy; | 	const vec2 rect_size = instance_rect.zw - instance_rect.xy; | ||||||
|  | 	const vec4 screen_position = vec4(projected_position, instance_depth, 1.0); | ||||||
| 
 | 
 | ||||||
| 	gl_Position = vec4(projected_position, instance_depth, 1.0); | 	gl_Position = screen_position; | ||||||
| 	color = instance_tint; | 	color = instance_tint; | ||||||
| 	uv = instance_rect.xy + (mesh_uv * rect_size); | 	uv = instance_rect.xy + (mesh_uv * rect_size); | ||||||
| } | } | ||||||
|  | |||||||
| @ -67,11 +67,11 @@ pub const Stage = struct { | |||||||
| 			float2, | 			float2, | ||||||
| 			float3, | 			float3, | ||||||
| 			float4, | 			float4, | ||||||
| 			int, | 			integer, | ||||||
| 			int2, | 			integer2, | ||||||
| 			int3, | 			integer3, | ||||||
| 			int4, | 			integer4, | ||||||
| 			mat4, | 			matrix4, | ||||||
| 		}; | 		}; | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| @ -82,29 +82,27 @@ pub const Stage = struct { | |||||||
| 		pub const max_uniforms = 16; | 		pub const max_uniforms = 16; | ||||||
| 
 | 
 | ||||||
| 		pub fn size(self: UniformBlock) usize { | 		pub fn size(self: UniformBlock) usize { | ||||||
| 			const alignment: usize = switch (self.layout) { |  | ||||||
| 				.std140 => 16, |  | ||||||
| 			}; |  | ||||||
| 
 |  | ||||||
| 			var accumulated_size: usize = 0; | 			var accumulated_size: usize = 0; | ||||||
| 
 | 
 | ||||||
| 			for (self.uniforms) |uniform| { | 			for (self.uniforms) |uniform| { | ||||||
| 				const type_size = @max(1, uniform.len) * @as(usize, switch (uniform.type) { | 				accumulated_size += @max(1, uniform.len) * @as(usize, switch (uniform.type) { | ||||||
| 					.float => 4, | 					.float => 4, | ||||||
| 					.float2 => 8, | 					.float2 => 8, | ||||||
| 					.float3 => 12, | 					.float3 => 12, | ||||||
| 					.float4 => 16, | 					.float4 => 16, | ||||||
| 					.int => 4, | 					.integer => 4, | ||||||
| 					.int2 => 8, | 					.integer2 => 8, | ||||||
| 					.int3 => 12, | 					.integer3 => 12, | ||||||
| 					.int4 => 16, | 					.integer4 => 16, | ||||||
| 					.mat4 => 64, | 					.matrix4 => 64, | ||||||
| 				}); | 				}); | ||||||
| 
 |  | ||||||
| 				accumulated_size += (type_size + (alignment - 1)) & ~(alignment - 1); |  | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			return accumulated_size; | 			const alignment: usize = switch (self.layout) { | ||||||
|  | 				.std140 => 16, | ||||||
|  | 			}; | ||||||
|  | 
 | ||||||
|  | 			return (accumulated_size + (alignment - 1)) & ~(alignment - 1); | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| @ -212,7 +210,7 @@ pub const Stage = struct { | |||||||
| 					.type = try switch (ext.spvc_type_get_basetype(member_type_handle)) { | 					.type = try switch (ext.spvc_type_get_basetype(member_type_handle)) { | ||||||
| 						ext.SPVC_BASETYPE_FP32 => switch (ext.spvc_type_get_vector_size(member_type_handle)) { | 						ext.SPVC_BASETYPE_FP32 => switch (ext.spvc_type_get_vector_size(member_type_handle)) { | ||||||
| 							4 => switch (ext.spvc_type_get_columns(member_type_handle)) { | 							4 => switch (ext.spvc_type_get_columns(member_type_handle)) { | ||||||
| 								4 => Uniform.Type.mat4, | 								4 => Uniform.Type.matrix4, | ||||||
| 								1 => Uniform.Type.float4, | 								1 => Uniform.Type.float4, | ||||||
| 								else => error.UnsupportedSPIRV, | 								else => error.UnsupportedSPIRV, | ||||||
| 							}, | 							}, | ||||||
| @ -224,10 +222,10 @@ pub const Stage = struct { | |||||||
| 						}, | 						}, | ||||||
| 
 | 
 | ||||||
| 						ext.SPVC_BASETYPE_INT32 => try switch (ext.spvc_type_get_vector_size(member_type_handle)) { | 						ext.SPVC_BASETYPE_INT32 => try switch (ext.spvc_type_get_vector_size(member_type_handle)) { | ||||||
| 							1 => Uniform.Type.int, | 							1 => Uniform.Type.integer, | ||||||
| 							2 => Uniform.Type.int2, | 							2 => Uniform.Type.integer2, | ||||||
| 							3 => Uniform.Type.int3, | 							3 => Uniform.Type.integer3, | ||||||
| 							4 => Uniform.Type.int4, | 							4 => Uniform.Type.integer4, | ||||||
| 							else => error.UnsupportedSPIRV, | 							else => error.UnsupportedSPIRV, | ||||||
| 						}, | 						}, | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -75,6 +75,7 @@ pub fn start_app(setup: Setup, options: Options) anyerror!void { | |||||||
| 	const app = try world.set_get_resource(App{ | 	const app = try world.set_get_resource(App{ | ||||||
| 		.events = &events, | 		.events = &events, | ||||||
| 		.target_frame_time = 1.0 / @as(f64, @floatFromInt(options.tick_rate)), | 		.target_frame_time = 1.0 / @as(f64, @floatFromInt(options.tick_rate)), | ||||||
|  | 		.elapsed_time = 0, | ||||||
| 		.is_running = true, | 		.is_running = true, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| @ -85,7 +86,8 @@ pub fn start_app(setup: Setup, options: Options) anyerror!void { | |||||||
| 	try setup(&world, events); | 	try setup(&world, events); | ||||||
| 	try world.run_event(events.load); | 	try world.run_event(events.load); | ||||||
| 
 | 
 | ||||||
| 	var ticks_previous = std.time.milliTimestamp(); | 	const ticks_initial = std.time.milliTimestamp(); | ||||||
|  | 	var ticks_previous = ticks_initial; | ||||||
| 	var accumulated_time = @as(f64, 0); | 	var accumulated_time = @as(f64, 0); | ||||||
| 
 | 
 | ||||||
| 	while (app.is_running) { | 	while (app.is_running) { | ||||||
| @ -93,6 +95,7 @@ pub fn start_app(setup: Setup, options: Options) anyerror!void { | |||||||
| 		const milliseconds_per_second = 1000.0; | 		const milliseconds_per_second = 1000.0; | ||||||
| 		const delta_time = @as(f64, @floatFromInt(ticks_current - ticks_previous)) / milliseconds_per_second; | 		const delta_time = @as(f64, @floatFromInt(ticks_current - ticks_previous)) / milliseconds_per_second; | ||||||
| 
 | 
 | ||||||
|  | 		app.elapsed_time = @as(f64, @floatFromInt(ticks_current - ticks_initial)) / milliseconds_per_second; | ||||||
| 		ticks_previous = ticks_current; | 		ticks_previous = ticks_current; | ||||||
| 		accumulated_time += delta_time; | 		accumulated_time += delta_time; | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user