Repository navigation
Expand file tree
/
Copy pathshaders.zig
More file actions
487 lines (436 loc) · 22.3 KB
/
Copy pathshaders.zig
File metadata and controls
487 lines (436 loc) · 22.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
//! The `openreliant.shaders` package ([#629](https://github.com/OpenReliant/openreliant/issues/629)),
//! for player scripts: GLSL functions from the calling mod that change how surfaces are lit.
//!
//! - A surface function changes a pixel's colour, normal, roughness, metalness, glow and alpha
//! before it is lit. It draws on the textures it names, on every lit surface where it says so,
//! and on the objects a script gives it (`object:set_surface`).
//! - A lighting function changes how much of each light reaches a pixel. One draws at a time: the
//! enabled one registered last.
//! - `register_surface` and `register_lighting` read the shader from the mod's files, with the files
//! it includes (`glsl.read`), and have the host compile it (`ShaderHost`). A shader that doesn't
//! compile is an error in the script, with the file and the line. Names are qualified with the
//! mod's, as `cel:ink`.
//! - A function is removed when the script that registered it stops. If a script fails to load,
//! the functions it registered are removed (`Registry.removeSince`).
//! - The host draws them on the GPU. Without a host, as in the tests, scripts can still register
//! them, and they draw nothing.
const std = @import("std");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.scripts);
const openreliant = @import("openreliant");
const ModSurface = openreliant.engine.surrender.surrenderlib.srtexture.ModSurface;
const create = openreliant.engine.game.create;
const atmosphere = create.atmosphere;
const max_objects = openreliant.engine.game.gameobj.max_objects;
const api = @import("api.zig");
const Call = api.Call;
const values = @import("values.zig");
const runtime_module = @import("runtime.zig");
const postprocessing = @import("postprocessing.zig");
const glsl = @import("glsl.zig");
const Context = runtime_module.Context;
const Object = openreliant.engine.hooks.Object;
/// The most functions the scripts can have registered at once.
pub const max_functions = 64;
/// The most textures a surface function names.
pub const max_textures = 32;
/// The numbers a function reads, as a post effect's (`postprocessing.Parameters`).
pub const parameter_count = postprocessing.parameter_count;
pub const Parameters = postprocessing.Parameters;
/// What a function changes.
pub const Kind = enum {
/// A pixel's surface, before it is lit.
surface,
/// How much of each light reaches a pixel.
lighting,
};
/// A surface function, as a script registers it.
pub const SurfaceDefinition = struct {
pub const script_name = "SurfaceFunction";
/// Its name, which the mod's name qualifies.
name: []const u8,
/// The mod's file that holds the function, such as `ink.glsl`.
shader: []const u8,
/// The textures it draws on, by file name, such as `pred_hull.tga`.
textures: values.List([]const u8, max_textures) = .{},
/// Whether it draws on every lit surface in the scene that has no surface function of its own.
everywhere: bool = false,
/// Whether the surfaces it draws on objects and textures blend by the alpha it sets, even where
/// the game draws them solid. They then draw sorted with the game's other blended draws.
see_through: bool = false,
/// Whether see-through surfaces still write depth, as a solid model does: true for a solid
/// model made see-through, so that of two models that cut into each other the nearer hides the
/// other; false for something like a glow or a cloud.
writes_depth: bool = true,
/// How much of the light of the surfaces it draws blooms, from 0, none, to 1, all of it, as
/// everything else blooms. It counts where they are drawn solid or added over what lies behind
/// them, not where they blend by their alpha, and the function can change it for each pixel.
bloom: f32 = 1,
/// The numbers it reads as `parameters`; those left out are 0.
parameters: Parameters = .{},
enabled: bool = true,
};
/// A lighting function, as a script registers it.
pub const LightingDefinition = struct {
pub const script_name = "LightingFunction";
name: []const u8,
/// The mod's file that holds the function, such as `bands.glsl`.
shader: []const u8,
parameters: Parameters = .{},
enabled: bool = true,
};
/// What compiles the mods' functions and draws them. The driver provides it.
pub const ShaderHost = struct {
context: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
/// Compiles the function of `kind` made of `parts`, its mod's file and the files it
/// includes (`glsl.read`), called `name`, and keeps it as a function of the host's.
add: *const fn (context: *anyopaque, kind: Kind, name: []const u8, parts: []const glsl.Part) Compiled,
/// Removes the function `function`.
remove: *const fn (context: *anyopaque, function: u16) void,
/// Draws `functions`, in the order they were registered, from now on.
update: *const fn (context: *anyopaque, functions: []const Function) void,
};
/// What compiling a function gave.
pub const Compiled = union(enum) {
/// The function added.
function: u16,
/// Why it didn't compile. The text is valid until the host's next call.
failed: []const u8,
};
};
/// A function as the host draws it.
pub const Function = struct {
function: u16,
kind: Kind,
parameters: [parameter_count]f32,
enabled: bool,
everywhere: bool,
see_through: bool,
writes_depth: bool,
bloom: f32,
textures: []const []const u8,
/// The surface function as a texture or every lit surface draws with it.
pub fn surface(function: Function) ModSurface {
return .{ .function = function.function, .parameters = function.parameters, .see_through = function.see_through, .writes_depth = function.writes_depth, .bloom = function.bloom };
}
};
/// A function registered.
const Entry = struct {
context: *Context,
name: runtime_module.Name,
kind: Kind,
parameters: [parameter_count]f32,
enabled: bool,
everywhere: bool,
see_through: bool = false,
writes_depth: bool = true,
bloom: f32 = 1,
/// Owned copies of the texture names.
textures: []const []const u8,
/// The host's function, or null if there is no host.
function: ?u16,
/// The surface function as an object draws with it, as the host compiled it, reading
/// `parameters`.
fn surface(entry: Entry, function: u16, parameters: [parameter_count]f32) ModSurface {
return .{ .function = function, .parameters = parameters, .see_through = entry.see_through, .writes_depth = entry.writes_depth, .bloom = entry.bloom };
}
};
/// The functions the scripts have registered.
pub const Registry = struct {
entries: std.ArrayList(Entry) = .empty,
/// What compiles and draws them, or null if nothing does, such as in a test.
host: ?ShaderHost = null,
/// The surface functions player scripts gave objects (`object:set_surface`), by slot, which
/// each drawn frame's copy of the objects takes up (`dress`). The game's objects never get
/// them.
given: [max_objects]?Given = @splat(null),
/// A surface function given to the object in a slot, and the slot's reuse count then
/// (`create.Objects.reuses`), so that the object that takes the slot next doesn't get it.
const Given = struct {
reuse: u32,
surface: ModSurface,
};
pub fn deinit(registry: *Registry, gpa: Allocator) void {
for (registry.entries.items) |entry| registry.release(gpa, entry);
registry.entries.deinit(gpa);
}
fn release(registry: *const Registry, gpa: Allocator, entry: Entry) void {
if (registry.host) |host| if (entry.function) |function| host.vtable.remove(host.context, function);
freeTextures(gpa, entry.textures);
}
/// Sets what compiles and draws the functions from now on, or null for nothing. Functions
/// already registered are removed from the old host and draw no more.
pub fn setHost(registry: *Registry, host: ?ShaderHost) void {
if (registry.host) |old| for (registry.entries.items) |*entry| {
if (entry.function) |function| old.vtable.remove(old.context, function);
entry.function = null;
};
registry.host = host;
registry.sync();
}
/// Removes the functions `context` registered from entry `first` on: all of them when its
/// script stops, or the ones it registered while failing to load.
pub fn removeSince(registry: *Registry, gpa: Allocator, context: *const Context, first: usize) void {
var kept: usize = @min(first, registry.entries.items.len);
const before = registry.entries.items.len;
for (registry.entries.items[kept..]) |entry| {
if (entry.context == context) {
registry.release(gpa, entry);
continue;
}
registry.entries.items[kept] = entry;
kept += 1;
}
registry.entries.shrinkRetainingCapacity(kept);
if (kept != before) registry.sync();
}
/// Removes every function `context` registered.
pub fn removeContext(registry: *Registry, gpa: Allocator, context: *const Context) void {
registry.removeSince(gpa, context, 0);
}
fn find(registry: *Registry, name: []const u8) ?*Entry {
for (registry.entries.items) |*entry| if (std.mem.eql(u8, entry.name.slice(), name)) return entry;
return null;
}
/// The surface function registered as `name`, qualified, as the host compiled it; null for any
/// other name, a lighting function's, or a function the host refused.
pub fn surfaceNamed(registry: *Registry, name: []const u8) ?ModSurface {
const entry = registry.find(name) orelse return null;
if (entry.kind != .surface) return null;
const function = entry.function orelse return null;
return entry.surface(function, @splat(0));
}
/// Gives each object of `all`, the drawn frame's copy of the objects, the surface function a
/// player script gave it (`object:set_surface`), over every part of its model. A function given
/// to an object that has gone since, whose slot another object takes now, is let go.
pub fn dress(registry: *Registry, all: *create.Objects) void {
for (®istry.given, &all.slots, all.reuses) |*given, *slot, reuse| {
const held = given.* orelse continue;
if (held.reuse != reuse) {
given.* = null;
continue;
}
if (slot.model) |*model| for (model.parts) |*part| {
part.object.surface = held.surface;
};
}
}
/// Lets go of the surface functions given to objects, as the player scripts stop.
pub fn forgetGiven(registry: *Registry) void {
registry.given = @splat(null);
}
/// What finds the surface functions the planets name for their atmospheres
/// (`game.create.atmosphere.SurfaceNames`).
pub fn surfaceNames(registry: *Registry) atmosphere.SurfaceNames {
return .{ .context = registry, .find = findSurface };
}
fn findSurface(context: *anyopaque, name: []const u8) ?ModSurface {
const registry: *Registry = @ptrCast(@alignCast(context));
return registry.surfaceNamed(name);
}
/// Tells the host what to draw from now on.
fn sync(registry: *const Registry) void {
const host = registry.host orelse return;
var buffer: [max_functions]Function = undefined;
host.vtable.update(host.context, registry.functions(&buffer));
}
/// The functions the host has, in the order they were registered, in `buffer`.
pub fn functions(registry: *const Registry, buffer: *[max_functions]Function) []const Function {
var count: usize = 0;
for (registry.entries.items) |entry| {
const function = entry.function orelse continue;
buffer[count] = .{
.function = function,
.kind = entry.kind,
.parameters = entry.parameters,
.enabled = entry.enabled,
.everywhere = entry.everywhere,
.see_through = entry.see_through,
.writes_depth = entry.writes_depth,
.bloom = entry.bloom,
.textures = entry.textures,
};
count += 1;
}
return buffer[0..count];
}
};
fn freeTextures(gpa: Allocator, textures: []const []const u8) void {
for (textures) |name| gpa.free(name);
gpa.free(textures);
}
/// What `openreliant.shaders` holds.
pub const package = struct {
pub const register_surface = api.Function("Registers a surface function: a GLSL function `surface` from the calling mod's file `shader`, which changes a pixel before it is lit. It draws on the `textures` it names, on every lit surface without a function of its own where `everywhere` is true, and on the objects given it with `object:set_surface`. `parameters` holds up to four numbers it reads, and `bloom`, from 0 to 1, how much of its surfaces' light blooms. Returns the function's name, qualified with the mod's. The file can include other files of the mod with `#include`. A shader that doesn't compile is an error, with its file and line. See the scripting guide for what the function reads and sets.", &.{"definition"}, registerSurface);
pub const register_lighting = api.Function("Registers a lighting function: a GLSL function `lighting` from the calling mod's file `shader`, which changes how much of each light reaches a pixel. One draws at a time: the enabled one registered last. `parameters` holds up to four numbers it reads. Returns the function's name, qualified with the mod's. The file can include other files of the mod with `#include`. A shader that doesn't compile is an error, with its file and line.", &.{"definition"}, registerLighting);
pub const set_enabled = api.Function("Turns the calling mod's function `name` on or off, by its own name or the qualified one. Returns whether the mod has it.", &.{ "name", "enabled" }, setEnabled);
pub const set_parameters = api.Function("Sets the numbers the calling mod's function `name` reads on its textures and everywhere it draws, up to four; those left out are 0. Returns whether the mod has it.", &.{ "name", "parameters" }, setParameters);
};
fn registryOf(call: Call) *Registry {
return &call.runtime().mod_shaders;
}
fn registerSurface(call: Call, given: SurfaceDefinition) []const u8 {
if (!(given.bloom >= 0 and given.bloom <= 1)) call.raise("bloom must be from 0 to 1, not {d}", .{given.bloom});
return register(call, .surface, given.name, given.shader, given.parameters, given.enabled, .{
.everywhere = given.everywhere,
.see_through = given.see_through,
.writes_depth = given.writes_depth,
.bloom = given.bloom,
.textures = given.textures.slice(),
});
}
fn registerLighting(call: Call, given: LightingDefinition) []const u8 {
return register(call, .lighting, given.name, given.shader, given.parameters, given.enabled, .{});
}
/// Where a surface function draws and how its surfaces blend (`SurfaceDefinition`); a lighting
/// function's are the defaults.
const Drawing = struct {
everywhere: bool = false,
see_through: bool = false,
writes_depth: bool = true,
bloom: f32 = 1,
textures: []const []const u8 = &.{},
};
fn register(call: Call, kind: Kind, local: []const u8, shader: []const u8, parameters: Parameters, enabled: bool, drawing: Drawing) []const u8 {
if (call.context.family != .player) call.raise("only player scripts can register functions", .{});
const scripts = call.runtime();
const gpa = scripts.gpa;
const registry = &scripts.mod_shaders;
var buffer: [runtime_module.max_name]u8 = undefined;
const name = runtime_module.Name.of(call.qualified(local, &buffer)).?;
if (registry.find(name.slice()) != null) call.raise("the function '{s}' is registered already", .{name.slice()});
if (registry.entries.items.len == max_functions) call.raise("at most {d} functions can be registered", .{max_functions});
registry.entries.ensureUnusedCapacity(gpa, 1) catch call.raise("out of memory", .{});
const mod = call.context.modOf();
var why: glsl.Why = .{};
var read = glsl.read(gpa, mod, shader, &why) catch |err| switch (err) {
error.OutOfMemory => call.raise("out of memory", .{}),
error.Failed => call.raise("{s}", .{why.text()}),
};
var file_buffer: [runtime_module.max_name * 2]u8 = undefined;
const file = std.mem.print(&file_buffer, "{s}/{s}", .{ mod.name, shader }) catch shader;
const compiled: ?ShaderHost.Compiled = if (registry.host) |host| host.vtable.add(host.context, kind, file, read.parts) else null;
// Freed before an error is raised, which skips what would free it.
read.deinit();
const function: ?u16 = if (compiled) |result| switch (result) {
.function => |made| made,
.failed => |message| call.raise("{s}", .{message}),
} else null;
// Copied last, as raising an error skips what would free them.
const owned = copyTextures(gpa, drawing.textures) catch {
if (registry.host) |host| if (function) |made| host.vtable.remove(host.context, made);
call.raise("out of memory", .{});
};
registry.entries.appendAssumeCapacity(.{
.context = call.context,
.name = name,
.kind = kind,
.parameters = parameters.padded(0),
.enabled = enabled,
.everywhere = drawing.everywhere,
.see_through = drawing.see_through,
.writes_depth = drawing.writes_depth,
.bloom = drawing.bloom,
.textures = owned,
.function = function,
});
registry.sync();
log.info("{s}: registered the {t} function {s}", .{ mod.name, kind, name.slice() });
return registry.entries.items[registry.entries.items.len - 1].name.slice();
}
fn copyTextures(gpa: Allocator, textures: []const []const u8) Allocator.Error![]const []const u8 {
const copies = try gpa.alloc([]const u8, textures.len);
var made: usize = 0;
errdefer {
for (copies[0..made]) |name| gpa.free(name);
gpa.free(copies);
}
for (textures, copies) |name, *copy| {
copy.* = try gpa.dupe(u8, name);
made += 1;
}
return copies;
}
fn setEnabled(call: Call, local: []const u8, enabled: bool) bool {
var buffer: [runtime_module.max_name]u8 = undefined;
const registry = registryOf(call);
const entry = registry.find(call.qualified(local, &buffer)) orelse return false;
entry.enabled = enabled;
registry.sync();
return true;
}
fn setParameters(call: Call, local: []const u8, given: Parameters) bool {
var buffer: [runtime_module.max_name]u8 = undefined;
const registry = registryOf(call);
const entry = registry.find(call.qualified(local, &buffer)) orelse return false;
entry.parameters = given.padded(0);
registry.sync();
return true;
}
/// `object:set_surface(name, parameters)`: draws the object with the surface function `name`, the
/// calling mod's by its own name or any mod's by the qualified one, reading `parameters`; nil
/// draws it with its textures' again. Returns false if no function of that name is registered.
/// The function is kept with the registry, and each drawn frame gives it to its copy of the
/// object (`Registry.dress`), however long the object stays in its slot.
pub fn setSurface(call: Call, object: Object, name: ?[]const u8, given: ?Parameters) bool {
if (call.context.family != .player) call.raise("only player scripts can set an object's surface function", .{});
const all = call.runtime().objects orelse call.raise("objects only exist while a game runs", .{});
var surface: ?ModSurface = null;
if (name) |wanted| {
var buffer: [runtime_module.max_name]u8 = undefined;
const qualified = call.named(wanted, &buffer) orelse return false;
const entry = registryOf(call).find(qualified) orelse return false;
if (entry.kind != .surface) call.raise("{s} is a lighting function", .{qualified});
const parameters = (given orelse Parameters{}).padded(0);
// Without a host the function draws nothing, and the object draws as it is.
if (entry.function) |function| surface = entry.surface(function, parameters);
}
const slot = object.slot();
registryOf(call).given[slot] = if (surface) |chosen| .{ .reuse = all.reuses[slot], .surface = chosen } else null;
return true;
}
/// A stand-in host for the tests, and the cel-shading example's functions, which the driver's
/// tests compile.
pub const testing = struct {
pub const cel_bands = @embedFile("cel-shading/bands.glsl");
pub const cel_ink = @embedFile("cel-shading/ink.glsl");
/// It "compiles" any function that doesn't contain `broken`, numbering them from 1, and records
/// what it is told.
pub const Host = struct {
added: u16 = 0,
/// How many parts the last function added came in.
parts: usize = 0,
removed: std.ArrayList(u16) = .empty,
/// What the last update drew: each function, by its number, its kind and whether it is on.
drawn: std.ArrayList(Function) = .empty,
pub fn deinit(host: *Host) void {
host.removed.deinit(std.testing.allocator);
host.drawn.deinit(std.testing.allocator);
}
pub fn shaderHost(held: *Host) ShaderHost {
return .{ .context = held, .vtable = &.{ .add = add, .remove = remove, .update = update } };
}
fn from(context: *anyopaque) *Host {
return @ptrCast(@alignCast(context));
}
fn add(context: *anyopaque, kind: Kind, name: []const u8, parts: []const glsl.Part) ShaderHost.Compiled {
_ = kind;
_ = name;
const held = from(context);
for (parts) |part| if (std.mem.find(u8, part.source, "broken") != null) return .{ .failed = "cel/broken.glsl:2: 'broken' : undeclared identifier" };
held.added += 1;
held.parts = parts.len;
return .{ .function = held.added };
}
fn remove(context: *anyopaque, function: u16) void {
from(context).removed.append(std.testing.allocator, function) catch {};
}
fn update(context: *anyopaque, functions: []const Function) void {
const held = from(context);
held.drawn.clearRetainingCapacity();
held.drawn.appendSlice(std.testing.allocator, functions) catch {};
}
};
};