renderer/metal: CRT effect, ugly hacky code

This commit is contained in:
Mitchell Hashimoto
2023-11-14 22:40:40 -08:00
parent 28246a80b8
commit c347148fd7
5 changed files with 383 additions and 13 deletions

View File

@ -257,7 +257,9 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
errdefer buf_instance.deinit();
// Initialize our shaders
var shaders = try Shaders.init(alloc, device, &.{});
var shaders = try Shaders.init(alloc, device, &.{
@embedFile("shaders/temp3.metal"),
});
errdefer shaders.deinit(alloc);
// Font atlas textures
@ -584,6 +586,36 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
// Get our drawable (CAMetalDrawable)
const drawable = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{});
// Make our intermediate texture
const target = target: {
const desc = init: {
const Class = objc.getClass("MTLTextureDescriptor").?;
const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
break :init id_init;
};
// Set our properties
desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm));
desc.setProperty("width", @as(c_ulong, @intCast(self.screen_size.?.width)));
desc.setProperty("height", @as(c_ulong, @intCast(self.screen_size.?.height)));
desc.setProperty(
"usage",
@intFromEnum(mtl.MTLTextureUsage.render_target) |
@intFromEnum(mtl.MTLTextureUsage.shader_read) |
@intFromEnum(mtl.MTLTextureUsage.shader_write),
);
const id = self.device.msgSend(
?*anyopaque,
objc.sel("newTextureWithDescriptor:"),
.{desc},
) orelse return error.MetalFailed;
break :target objc.Object.fromId(id);
};
defer target.msgSend(void, objc.sel("release"), .{});
// If our font atlas changed, sync the texture data
if (self.font_group.atlas_greyscale.modified) {
try syncAtlasTexture(self.device, &self.font_group.atlas_greyscale, &self.texture_greyscale);
@ -620,10 +652,10 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
// Ghostty in XCode in debug mode it returns a CaptureMTLDrawable
// which ironically doesn't implement CAMetalDrawable as a
// property so we just send a message.
const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{});
//const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{});
attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear));
attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store));
attachment.setProperty("texture", texture);
attachment.setProperty("texture", target.value);
attachment.setProperty("clearColor", mtl.MTLClearColor{
.red = @as(f32, @floatFromInt(self.current_background_color.r)) / 255,
.green = @as(f32, @floatFromInt(self.current_background_color.g)) / 255,
@ -659,10 +691,132 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]);
}
{
// MTLRenderPassDescriptor
const desc = desc: {
const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?;
const desc = MTLRenderPassDescriptor.msgSend(
objc.Object,
objc.sel("renderPassDescriptor"),
.{},
);
// Set our color attachment to be our drawable surface.
const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
{
const attachment = attachments.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 0)},
);
// Texture is a property of CAMetalDrawable but if you run
// Ghostty in XCode in debug mode it returns a CaptureMTLDrawable
// which ironically doesn't implement CAMetalDrawable as a
// property so we just send a message.
const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{});
attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear));
attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store));
attachment.setProperty("texture", texture);
attachment.setProperty("clearColor", mtl.MTLClearColor{
.red = 0,
.green = 0,
.blue = 0,
.alpha = 1,
});
}
break :desc desc;
};
// MTLRenderCommandEncoder
const encoder = buffer.msgSend(
objc.Object,
objc.sel("renderCommandEncoderWithDescriptor:"),
.{desc.value},
);
defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
try self.drawPostShader(encoder, target.value);
}
buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value});
buffer.msgSend(void, objc.sel("commit"), .{});
}
var post_time: f32 = 1;
fn drawPostShader(
self: *Metal,
encoder: objc.Object,
texture: objc.c.id,
) !void {
// Use our image shader pipeline
encoder.msgSend(
void,
objc.sel("setRenderPipelineState:"),
.{self.shaders.post_pipelines[0].value},
);
// Set our uniform, which is the only shared buffer
encoder.msgSend(
void,
objc.sel("setVertexBytes:length:atIndex:"),
.{
@as(*const anyopaque, @ptrCast(&self.uniforms)),
@as(c_ulong, @sizeOf(@TypeOf(self.uniforms))),
@as(c_ulong, 1),
},
);
const Buffer = mtl_buffer.Buffer(mtl_shaders.PostUniforms);
var buf = try Buffer.initFill(self.device, &.{.{
.resolution = .{
@floatFromInt(self.screen_size.?.width),
@floatFromInt(self.screen_size.?.height),
1,
},
.time = post_time,
.time_delta = 1,
.frame_rate = 1,
.frame = 1,
.channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
.channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
.mouse = .{ 0, 0, 0, 0 },
.date = .{ 0, 0, 0, 0 },
.sample_rate = 1,
}});
defer buf.deinit();
post_time += 1;
// Set our buffer
encoder.msgSend(
void,
objc.sel("setFragmentBuffer:offset:atIndex:"),
.{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) },
);
encoder.msgSend(
void,
objc.sel("setFragmentTexture:atIndex:"),
.{
texture,
@as(c_ulong, 0),
},
);
// Draw!
encoder.msgSend(
void,
objc.sel("drawPrimitives:vertexStart:vertexCount:"),
.{
@intFromEnum(mtl.MTLPrimitiveType.triangle_strip),
@as(c_ulong, 0),
@as(c_ulong, 4),
},
);
}
fn drawImagePlacements(
self: *Metal,
encoder: objc.Object,

View File

@ -57,6 +57,7 @@ pub const MTLVertexStepFunction = enum(c_ulong) {
/// https://developer.apple.com/documentation/metal/mtlpixelformat?language=objc
pub const MTLPixelFormat = enum(c_ulong) {
r8unorm = 10,
rgba8unorm = 70,
rgba8uint = 73,
bgra8unorm = 80,
};
@ -98,6 +99,15 @@ pub const MTLBlendOperation = enum(c_ulong) {
max = 4,
};
/// https://developer.apple.com/documentation/metal/mtltextureusage?language=objc<D-j>
pub const MTLTextureUsage = enum(c_ulong) {
unknown = 0,
shader_read = 1,
shader_write = 2,
render_target = 4,
pixel_format_view = 8,
};
/// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc
/// (incomplete, we only use this mode so we just hardcode it)
pub const MTLResourceStorageModeShared: c_ulong = @intFromEnum(MTLStorageMode.shared) << 4;

View File

@ -49,6 +49,7 @@ pub const Shaders = struct {
const post_pipelines: []const objc.Object = initPostPipelines(
alloc,
device,
library,
post_shaders,
) catch |err| err: {
// If an error happens while building postprocess shaders we
@ -126,6 +127,20 @@ pub const Uniforms = extern struct {
strikethrough_thickness: f32,
};
/// The uniforms used for custom postprocess shaders.
pub const PostUniforms = extern struct {
resolution: [3]f32 align(16),
time: f32 align(4),
time_delta: f32 align(4),
frame_rate: f32 align(4),
frame: i32 align(4),
channel_time: [4][4]f32 align(16),
channel_resolution: [4][4]f32 align(16),
mouse: [4]f32 align(16),
date: [4]f32 align(16),
sample_rate: f32 align(4),
};
/// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders.
fn initLibrary(device: objc.Object) !objc.Object {
// Hardcoded since this file isn't meant to be reusable.
@ -157,6 +172,7 @@ fn initLibrary(device: objc.Object) !objc.Object {
fn initPostPipelines(
alloc: Allocator,
device: objc.Object,
library: objc.Object,
shaders: []const [:0]const u8,
) ![]const objc.Object {
// If we have no shaders, do nothing.
@ -177,7 +193,7 @@ fn initPostPipelines(
// Build each shader. Note we don't use "0.." to build our index
// because we need to keep track of our length to clean up above.
for (shaders) |source| {
pipelines[i] = try initPostPipeline(device, source);
pipelines[i] = try initPostPipeline(device, library, source);
i += 1;
}
@ -185,9 +201,13 @@ fn initPostPipelines(
}
/// Initialize a single custom shader pipeline from shader source.
fn initPostPipeline(device: objc.Object, data: [:0]const u8) !objc.Object {
fn initPostPipeline(
device: objc.Object,
library: objc.Object,
data: [:0]const u8,
) !objc.Object {
// Create our library which has the shader source
const library = library: {
const post_library = library: {
const source = try macos.foundation.String.createWithBytes(
data,
.utf8,
@ -196,22 +216,75 @@ fn initPostPipeline(device: objc.Object, data: [:0]const u8) !objc.Object {
defer source.release();
var err: ?*anyopaque = null;
const library = device.msgSend(
const post_library = device.msgSend(
objc.Object,
objc.sel("newLibraryWithSource:options:error:"),
.{ source, @as(?*anyopaque, null), &err },
);
try checkError(err);
errdefer library.msgSend(void, objc.sel("release"), .{});
errdefer post_library.msgSend(void, objc.sel("release"), .{});
break :library library;
break :library post_library;
};
// TODO: need to do this once we set the pipeline
//defer library.msgSend(void, objc.sel("release"), .{});
defer post_library.msgSend(void, objc.sel("release"), .{});
// TODO: need to implement the actual pipeline
// Get our vertex and fragment functions
const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes(
"post_vertex",
.utf8,
false,
);
defer str.release();
return library;
const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
break :func_vert objc.Object.fromId(ptr.?);
};
const func_frag = func_frag: {
const str = try macos.foundation.String.createWithBytes(
"main0",
.utf8,
false,
);
defer str.release();
const ptr = post_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
break :func_frag objc.Object.fromId(ptr.?);
};
// Create our descriptor
const desc = init: {
const Class = objc.getClass("MTLRenderPipelineDescriptor").?;
const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
break :init id_init;
};
desc.setProperty("vertexFunction", func_vert);
desc.setProperty("fragmentFunction", func_frag);
// Set our color attachment
const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
{
const attachment = attachments.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 0)},
);
// Value is MTLPixelFormatBGRA8Unorm
attachment.setProperty("pixelFormat", @as(c_ulong, 80));
}
// Make our state
var err: ?*anyopaque = null;
const pipeline_state = device.msgSend(
objc.Object,
objc.sel("newRenderPipelineStateWithDescriptor:error:"),
.{ desc, &err },
);
try checkError(err);
return pipeline_state;
}
/// Initialize the cell render pipeline for our shader library.

View File

@ -253,3 +253,20 @@ fragment float4 image_fragment(
uint4 rgba = image.sample(textureSampler, in.tex_coord);
return float4(rgba) / 255.0f;
}
//-------------------------------------------------------------------
// Post Shader
//-------------------------------------------------------------------
#pragma mark - Post Shader
struct PostVertexOut {
float4 position [[ position ]];
};
constant float2 post_pos[4] = { {-1,-1}, {1,-1}, {-1,1}, {1,1 } };
vertex PostVertexOut post_vertex(uint id [[ vertex_id ]]) {
PostVertexOut out;
out.position = float4(post_pos[id], 0, 1);
return out;
}

View File

@ -0,0 +1,116 @@
#pragma clang diagnostic ignored "-Wmissing-prototypes"
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
// Implementation of the GLSL mod() function, which is slightly different than Metal fmod()
template<typename Tx, typename Ty>
inline Tx mod(Tx x, Ty y)
{
return x - y * floor(x / y);
}
struct Globals
{
float3 iResolution;
float iTime;
float iTimeDelta;
float iFrameRate;
int iFrame;
float4 iChannelTime[4];
float3 iChannelResolution[4];
float4 iMouse;
float4 iDate;
float iSampleRate;
};
struct main0_out
{
float4 _fragColor [[color(0)]];
};
static inline __attribute__((always_inline))
float2 curve(thread float2& uv)
{
uv = (uv - float2(0.5)) * 2.0;
uv *= 1.10000002384185791015625;
uv.x *= (1.0 + pow(abs(uv.y) / 5.0, 2.0));
uv.y *= (1.0 + pow(abs(uv.x) / 4.0, 2.0));
uv = (uv / float2(2.0)) + float2(0.5);
uv = (uv * 0.920000016689300537109375) + float2(0.039999999105930328369140625);
return uv;
}
static inline __attribute__((always_inline))
void mainImage(thread float4& fragColor, thread const float2& fragCoord, constant Globals& _89, texture2d<float> iChannel0, sampler iChannel0Smplr)
{
float2 q = fragCoord / float2(_89.iResolution[0], _89.iResolution[1]);
float2 uv = q;
float2 param = uv;
float2 _100 = curve(param);
uv = _100;
float3 oricol = iChannel0.sample(iChannel0Smplr, float2(q.x, q.y)).xyz;
float x = ((sin((0.300000011920928955078125 * _89.iTime) + (uv.y * 21.0)) * sin((0.699999988079071044921875 * _89.iTime) + (uv.y * 29.0))) * sin((0.300000011920928955078125 + (0.3300000131130218505859375 * _89.iTime)) + (uv.y * 31.0))) * 0.001700000022538006305694580078125;
float3 col;
col.x = iChannel0.sample(iChannel0Smplr, float2((x + uv.x) + 0.001000000047497451305389404296875, uv.y + 0.001000000047497451305389404296875)).x + 0.0500000007450580596923828125;
col.y = iChannel0.sample(iChannel0Smplr, float2((x + uv.x) + 0.0, uv.y - 0.00200000009499490261077880859375)).y + 0.0500000007450580596923828125;
col.z = iChannel0.sample(iChannel0Smplr, float2((x + uv.x) - 0.00200000009499490261077880859375, uv.y + 0.0)).z + 0.0500000007450580596923828125;
col.x += (0.07999999821186065673828125 * iChannel0.sample(iChannel0Smplr, ((float2(x + 0.02500000037252902984619140625, -0.02700000070035457611083984375) * 0.75) + float2(uv.x + 0.001000000047497451305389404296875, uv.y + 0.001000000047497451305389404296875))).x);
col.y += (0.0500000007450580596923828125 * iChannel0.sample(iChannel0Smplr, ((float2(x + (-0.02199999988079071044921875), -0.0199999995529651641845703125) * 0.75) + float2(uv.x + 0.0, uv.y - 0.00200000009499490261077880859375))).y);
col.z += (0.07999999821186065673828125 * iChannel0.sample(iChannel0Smplr, ((float2(x + (-0.0199999995529651641845703125), -0.017999999225139617919921875) * 0.75) + float2(uv.x - 0.00200000009499490261077880859375, uv.y + 0.0))).z);
col = fast::clamp((col * 0.60000002384185791015625) + (((col * 0.4000000059604644775390625) * col) * 1.0), float3(0.0), float3(1.0));
float vig = 0.0 + ((((16.0 * uv.x) * uv.y) * (1.0 - uv.x)) * (1.0 - uv.y));
col *= float3(pow(vig, 0.300000011920928955078125));
col *= float3(0.949999988079071044921875, 1.0499999523162841796875, 0.949999988079071044921875);
col *= 2.7999999523162841796875;
float scans = fast::clamp(0.3499999940395355224609375 + (0.3499999940395355224609375 * sin((3.5 * _89.iTime) + ((uv.y * _89.iResolution[1u]) * 1.5))), 0.0, 1.0);
float s = pow(scans, 1.7000000476837158203125);
col *= float3(0.4000000059604644775390625 + (0.699999988079071044921875 * s));
col *= (1.0 + (0.00999999977648258209228515625 * sin(110.0 * _89.iTime)));
bool _352 = uv.x < 0.0;
bool _359;
if (!_352)
{
_359 = uv.x > 1.0;
}
else
{
_359 = _352;
}
if (_359)
{
col *= 0.0;
}
bool _366 = uv.y < 0.0;
bool _373;
if (!_366)
{
_373 = uv.y > 1.0;
}
else
{
_373 = _366;
}
if (_373)
{
col *= 0.0;
}
col *= (float3(1.0) - (float3(fast::clamp((mod(fragCoord.x, 2.0) - 1.0) * 2.0, 0.0, 1.0)) * 0.64999997615814208984375));
float comp = smoothstep(0.100000001490116119384765625, 0.89999997615814208984375, sin(_89.iTime));
fragColor = float4(col, 1.0);
}
fragment main0_out main0(constant Globals& _89 [[buffer(0)]], texture2d<float> iChannel0 [[texture(0)]], float4 gl_FragCoord [[position]])
{
constexpr sampler iChannel0Smplr(address::clamp_to_edge, filter::linear);
main0_out out = {};
float2 param_1 = gl_FragCoord.xy;
float4 param;
mainImage(param, param_1, _89, iChannel0, iChannel0Smplr);
out._fragColor = param;
return out;
}