1-- Copyright: 2015-2017, Björn Ståhl
2-- License: 3-Clause BSD
3-- Reference: http://durden.arcan-fe.com
4-- Description: Shader compilation and setup.
5
6if (SHADER_LANGUAGE == "GLSL120") then
7local old_build = build_shader;
8function build_shader(vertex, fragment, label)
9	vertex = vertex and ("#define VERTEX\n" .. vertex) or nil;
10	fragment = fragment and ([[
11		#ifdef GL_ES
12			#ifdef GL_FRAGMENT_PRECISION_HIGH
13				precision highp float;
14			#else
15				precision mediump float;
16			#endif
17		#else
18			#define lowp
19			#define mediump
20			#define highp
21		#endif
22	]] .. fragment) or nil;
23
24--
25--  [ dump to get real line numbers ]
26--	local debug = string.split(fragment, "\n");
27--	for i,v in ipairs(debug) do print(i, v); end
28--
29--
30	return old_build(vertex, fragment, label);
31end
32end
33
34local shdrtbl = {
35	effect = {},
36	ui = {},
37	display = {},
38	audio = {},
39	simple = {}
40};
41
42local groups = {"effect", "ui", "display", "audio", "simple"};
43
44function shdrmgmt_scan()
45 	for a,b in ipairs(groups) do
46 		local path = string.format("shaders/%s/", b);
47
48		for i,j in ipairs(glob_resource(path .. "*.lua", APPL_RESOURCE)) do
49			local res = system_load(path .. j, false);
50			if (res) then
51				res = res();
52				if (not res or type(res) ~= "table" or res.version ~= 1) then
53					warning("shader " .. j .. " failed validation");
54				else
55					local key = string.sub(j, 1, string.find(j, '.', 1, true)-1);
56					shdrtbl[b][key] = res;
57				end
58		else
59				warning("error parsing " .. path .. j);
60 			end
61		end
62	end
63end
64
65shdrmgmt_scan();
66
67local function set_uniform(dstid, name, typestr, vals, source)
68	local len = string.len(typestr);
69	if (type(vals) == "table" and len ~= #vals) or
70		(type(vals) ~= "table" and len > 1) then
71		warning("set_uniform called from broken source: " .. source);
72 		return false;
73	end
74	if (type(vals) == "table") then
75		shader_uniform(dstid, name, typestr, unpack(vals));
76 	else
77		shader_uniform(dstid, name, typestr, vals);
78	end
79	return true;
80end
81
82local function load_from_file(relp, lim, defs)
83	local res = {};
84	if (open_rawresource(relp)) then
85		if defs then
86			for k,v in ipairs(defs) do
87				table.insert(res, "#define " .. v);
88			end
89		end
90
91		local line = read_rawresource();
92		while (line ~= nil and lim -1 ~= 0) do
93			table.insert(res, line);
94			line = read_rawresource();
95			lim = lim - 1;
96		end
97		close_rawresource();
98	else
99		warning(string.format("shader, load from file: %s failed, EEXIST", relp));
100	end
101
102	return table.concat(res, "\n");
103end
104
105local function setup_shader(shader, name, group)
106	if (shader.shid) then
107		return true;
108	end
109
110-- ugly non-blocking read (note, this does not cover variants)
111	if (not shader.vert and shader.vert_source) then
112		shader.vert = load_from_file(string.format(
113			"shaders/%s/%s", group, shader.vert_source), 1000, shader.vert_defs);
114	end
115
116	if (not shader.frag  and shader.frag_source) then
117		shader.frag = load_from_file(string.format(
118			"shaders/%s/%s", group, shader.frag_source), 1000, shader.vert_defs);
119	end
120
121	local dvf = (shader.vert and
122		type(shader.vert == "table") and shader.vert[SHADER_LANGUAGE])
123		and shader.vert[SHADER_LANGUAGE] or shader.vert;
124
125	local dff = (shader.frag and
126		type(shader.frag == "table") and shader.frag[SHADER_LANGUAGE])
127		and shader.frag[SHADER_LANGUAGE] or shader.frag;
128
129	shader.shid = build_shader(dvf, dff, group.."_"..name);
130	if (not shader.shid) then
131		shader.broken = true;
132		warning("building shader failed for " .. group.."_"..name);
133	return false;
134	end
135-- this is not very robust, bad written shaders will yield fatal()
136	for k,v in pairs(shader.uniforms) do
137		set_uniform(shader.shid, k, v.utype, v.default, name .. "-" .. k);
138	end
139	return true;
140end
141
142local function preload_effect_shader(shdr, group, name)
143	for _,v in ipairs(shdr.passes) do
144		if (not v.shid) then
145			setup_shader(v, name, group);
146		end
147		if (not shdr.scale) then
148			shdr.scale = {1, 1};
149		end
150		if (not shdr.filter) then
151			shdr.filter = "bilinear";
152		end
153		if (not shdr.maps) then
154			shdr.maps = {};
155		else
156-- asynch- shader lookup maps
157			for i,v in ipairs(shdr.maps) do
158				if (type(v) == "string") then
159					if (v == ":source") then
160						shdr.maps[i] = function(src)
161							local surf = null_surface(1, 1);
162							image_sharestorage(src, surf);
163							return surf;
164						end
165					else
166						shdr.maps[i] = load_image_asynch(
167							string.format("shaders/lut/%s", v),
168-- defer shader application if the LUT can't be loaded?
169							function() end
170						);
171					end
172				end
173			end
174		end
175	end
176end
177
178-- for display, state is actually the display name
179local function dsetup(shader, dst, group, name, state)
180	if (not setup_shader(shader, dst, name)) then
181		return;
182	end
183
184	if (not shader.states) then
185		shader.states = {};
186	end
187
188	if (not shader.states[state]) then
189		shader.states[state] = shader_ugroup(shader.shid);
190	end
191	image_shader(dst, shader.states[state]);
192end
193
194local function filter_strnum(fltstr)
195	if (fltstr == "bilinear") then
196		return FILTER_BILINEAR;
197	elseif (fltstr == "linear") then
198		return FILTER_LINEAR;
199	else
200		return FILTER_NONE;
201	end
202end
203
204local function ssetup(shader, dst, group, name, state)
205	if (not shader.shid) then
206		setup_shader(shader, name, group);
207
208-- states inherit shaders, define different uniform values
209		if (shader.states) then
210			for k,v in pairs(shader.states) do
211				shader.states[k].shid = shader_ugroup(shader.shid);
212
213				for i,j in pairs(v.uniforms) do
214					set_uniform(v.shid, i, shader.uniforms[i].utype, j,
215						string.format("%s-%s-%s", name, k, i));
216				end
217			end
218		end
219	end
220-- now the shader exists, apply
221	local shid = ((state and shader.states and shader.states[state]) and
222		shader.states[state].shid) or shader.shid;
223
224	if (valid_vid(dst)) then
225		image_shader(dst, shid);
226	end
227end
228
229local function esetup(shader, dst, group, name)
230	if (not shader.passes or #shader.passes == 0) then
231		return;
232	end
233
234-- Special shortpath, only one pass - used when we want global settings but
235-- otherwise the behavior of a simple shader
236	if (#shader.passes == 1 and shader.no_rendertarget) then
237		return ssetup(shader.passes[1], dst, group, name);
238	end
239
240-- Track the order in which the rendertargets are created. This is needed as
241-- each rendertarget is setup with manual update controls as a means of synching
242-- with the frame delivery rate of the source.
243	local rtgt_list = {};
244
245-- the process of taking a pass description, creating an intermediate FBO
246-- applying the pass shader and returning the outcome. Subtle edge conditions
247-- to look out for here.
248	local build_pass = function(invid, pass)
249		local props = image_storage_properties(invid);
250		local fmt = ALLOC_QUALITY_NORMAL;
251
252		if (pass.float) then
253			fmt = ALLOC_QUALITY_FLOAT16;
254		elseif (pass.float32) then
255			fmt = ALLOC_QUALITY_FLOAT32;
256		end
257		if (pass.filter) then
258			image_texfilter(invid, filter_strnum(pass.filter));
259		end
260
261-- min-clamp as there's a limit for the rendertarget backend store,
262-- note that scaling doesn't work with all modes (e.g. autocrop) or client types
263		local outw = math.clamp(props.width * pass.scale[1], 32);
264		local outh = math.clamp(props.height * pass.scale[2], 32);
265
266		local outvid = alloc_surface(outw, outh, true, fmt);
267		if (not valid_vid(outvid)) then
268			return invid;
269		end
270
271-- for the passes that require lookup textures, asynch- preloaded or through
272-- a function, switch the invid to a multitextured frameset and assign the slots
273-- accordingly.
274		local tmp_vid = null_surface(1, 1);
275		if valid_vid(tmp_vid) then
276			if (#pass.maps > 0) then
277				image_framesetsize(invid, #pass.maps + 1, FRAMESET_MULTITEXTURE);
278				for i,v in ipairs(pass.maps) do
279					if type(v) == "function" then
280						v(dst, tmp_vid);
281					elseif valid_vid(v) then
282						image_sharestorage(v, tmp_vid);
283-- fallback to source store if the maps were setup wrong
284					else
285						image_sharestorage(dst, tmp_vid);
286					end
287					set_image_as_frame(pass.maps, tmp_vid, i);
288				end
289			end
290			delete_image(tmp_vid);
291		end
292
293-- sanity checks and resource loading/preloading
294		define_rendertarget(outvid, {invid},
295			RENDERTARGET_DETACH, RENDERTARGET_NOSCALE, 0);
296		image_shader(invid, pass.shid);
297		resize_image(invid, outw, outh);
298		move_image(invid, 0, 0);
299		show_image({invid, outvid});
300		table.insert(rtgt_list, outvid);
301		rendertarget_forceupdate(outvid);
302		return outvid;
303	end
304
305-- this is currently quite wasteful, there is a blit-out copy stage in order
306-- to get an output buffer that can simply be sharestorage()d into the canvas
307-- slot rather than all the complications with swap-in-out.
308	local function build_passes()
309		local props = image_storage_properties(dst);
310		local invid = null_surface(props.width, props.height);
311		image_sharestorage(dst, invid);
312
313		for i=1,#shader.passes do
314			invid = build_pass(invid, shader.passes[i]);
315		end
316
317-- chain finished and stored in invid, final blitout pass so we have a
318-- shareable storage format
319		local outprops = image_storage_properties(invid);
320		local outvid = alloc_surface(outprops.width, outprops.height);
321		define_rendertarget(outvid, {invid},
322			RENDERTARGET_DETACH, RENDERTARGET_NOSCALE, 0);
323		table.insert(rtgt_list, outvid);
324--		show_image(outvid);
325		if (shader.filter) then
326			image_texfilter(outvid, filter_strnum(shader.filter));
327		end
328		return outvid;
329	end
330
331	preload_effect_shader(shader, group, name);
332	local outvid = build_passes();
333	rendertarget_forceupdate(outvid);
334	hide_image(outvid);
335
336-- return a reference to the video object, a refresh function and a
337-- rebuild-or-destroy function.
338	return outvid, function()
339		for i,v in ipairs(rtgt_list) do
340			rendertarget_forceupdate(v);
341		end
342	end,
343	function(vid, destroy)
344		for i,v in ipairs(rtgt_list) do
345			delete_image(v);
346		end
347		rtgt_list = {};
348-- this is unnecessarily expensive, better approach would be to re-enumerate
349-- the passes and just resize rendertarget and inputs / outputs
350		if (not destroy and valid_vid(vid)) then
351			dst = vid;
352			return build_passes();
353		end
354	end;
355end
356
357-- note: boolean and 4x4 matrices are currently ignored
358local utype_lut = {
359i = 1, f = 1, ff = 1, fff = 1, ffff = 1
360};
361
362local function add_stateref(res, uniforms, shid)
363	for k,v in pairs(uniforms) do
364		if (not v.ignore) then
365			table.insert(res, {
366			name = k,
367			label = v.label,
368			kind = "value",
369			hint = (type(v.default) == "table" and
370				table.concat(v.default, " ")) or tostring(v.default),
371			eval = function()
372				return utype_lut[v.utype] ~= nil;
373			end,
374			validator = suppl_valid_typestr(v.utype, v.low, v.high, v.default),
375			handler = function(ctx, val)
376				shader_uniform(shid, k, v.utype, unpack(
377					suppl_unpack_typestr(v.utype, val, v.low, v.high)));
378			end
379		});
380		end
381	end
382end
383
384-- the different shader types:
385-- 'ui' Has states that need to be forwarded. Right now, there is just
386-- one shared for all UI elements because the delete_shader approach for
387-- ugroups is faulty, so we'd have problems after 64k such changes but we
388-- do want to be able to forward more window specific parameters, like
389-- privilege level and so on.
390--
391-- 'simple, audio' are treated as ui, though won't have an instanced state.
392--
393-- displays are inherently single pass.
394--
395-- 'effect' is more complicated as it needs to support multiple passes
396-- with indirect offscreen rendering and will be chainable in the future.
397--
398local function smenu(shdr, grp, name)
399	if (not shdr.uniforms) then
400		return;
401	end
402
403	local found = false;
404	for k,v in pairs(shdr.uniforms) do
405		if (not v.ignore) then
406			found = true;
407			break;
408		end
409	end
410	if (not found) then
411		return;
412	end
413
414	local res = {
415	};
416
417	if (shdr.states) then
418		for k,v in pairs(shdr.states) do
419-- build even if it hasn't been used yet, otherwise this might cause menu
420-- entries not being available at start - and add stateref needs a shid to
421-- reference the uniform to
422			if (not v.shid and not v.broken) then
423				local nsrf = null_surface(1, 1);
424				ssetup(shdr, nsrf, grp, name);
425				delete_image(nsrf);
426			end
427
428			if (v.shid) then
429				table.insert(res, {
430					name = "state_" .. k,
431					label = k,
432					kind = "action",
433					submenu = true,
434					handler = function()
435						local res = {};
436						add_stateref(res, shdr.uniforms, v.shid);
437						return res;
438					end
439				});
440			end
441		end
442	else
443		add_stateref(res, shdr.uniforms, shdr.shid);
444	end
445
446	return res;
447end
448
449local function emenu(shdr, grp, name, state)
450	if (not shdr.passes or #shdr.passes == 0) then
451		return {};
452	end
453
454	local get_pass_menu = function(pass)
455		local res = {};
456		if (not pass.shid) then
457			setup_shader(pass, name, grp);
458		end
459		add_stateref(res, pass.uniforms, pass.shid);
460		return res;
461	end
462
463	if (#shdr.passes == 1) then
464		return get_pass_menu(shdr.passes[1]);
465	end
466
467	local res = {};
468	for i,pass in ipairs(shdr.passes) do
469		table.insert(res, {
470			submenu = true,
471			kind = "action",
472			name = "pass_" .. tostring(i),
473-- dynamic call this as it might trigger shader compilation which scales poorly
474			handler = function() return get_pass_menu(pass); end,
475			label = tostring(i)
476		});
477	end
478	return res;
479end
480
481local function dmenu(shdr, grp, name, state)
482	local res = {};
483	if (not shdr.uniforms) then
484		return res;
485	end
486
487	if (not shdr.states[state]) then
488		warning("display shader does not have matching display");
489		return res;
490	end
491
492	local found = false;
493	for k,v in pairs(shdr.uniforms) do
494		if (not v.ignore) then
495			found = true;
496			break;
497		end
498	end
499
500	if (not found) then
501		return res;
502	end
503
504	add_stateref(res, shdr.uniforms, shdr.states[state]);
505	return res;
506end
507
508-- argument one [ setup ], argument two, [ configuration menu ]
509local fmtgroups = {
510	ui = {ssetup, smenu},
511	effect = {esetup, emenu},
512	display = {dsetup, dmenu},
513	audio = {ssetup, smenu},
514	simple = {ssetup, smenu}
515};
516
517-- Prepare a shader with the rules applies from [group, name] in the optional
518-- state [state]. If the group is [effect] it will return the output of the
519-- chain using [dst] as initial input - if this is different from [dst] it is
520-- to be treated as a dynamically allocated/resolution sensitive effect chain
521-- with the last stage of the chain applied as effect.
522function shader_setup(dst, group, name, state)
523	if (not fmtgroups[group]) then
524		group = group and group or "no group";
525		warning("shader_setup called with unknown group " .. group);
526		return dst;
527	end
528
529	if (not shdrtbl[group] or not shdrtbl[group][name]) then
530		warning(string.format(
531			"shader_setup called with unknown group(%s) or name (%s) ",
532			group and group or "nil",
533			name and name or "nil"
534		));
535		return dst;
536	end
537
538	return fmtgroups[group][1](shdrtbl[group][name], dst, group, name, state);
539end
540
541-- workaround for not being able to expose custom arguments from normal _setup
542function shader_ui_lookup(dst, group, name, state)
543	local a, b, c = fmtgroups[group][1](shdrtbl[group][name], dst, group, name, state);
544	local shid = shdrtbl[group][name].shid;
545	return shid, a, b, c;
546end
547
548function shader_uform_menu(name, group, state)
549	if (not fmtgroups[group]) then
550		warning("shader_setup called with unknown group " .. group);
551		return {};
552	end
553
554	if (not shdrtbl[group] or not shdrtbl[group][name]) then
555		warning(string.format(
556			"shader_setup called with unknown group(%s) or name (%s) ",
557			group and group or "nil",
558			name and name or "nil"
559		));
560		return {};
561	end
562
563	return fmtgroups[group][2](shdrtbl[group][name], group, name, state);
564end
565
566-- update shader [sname] in group [domain] for the uniform [uname],
567-- targetting either the global [states == nil] or each individual
568-- instanced ugroup in [states].
569function shader_update_uniform(sname, domain, uname, args, states)
570	assert(shdrtbl[domain]);
571	assert(shdrtbl[domain][sname]);
572	local shdr = shdrtbl[domain][sname];
573	if (not states) then
574		states = {"default"};
575	end
576
577	for k,v in ipairs(states) do
578		local dstid, dstuni;
579-- special handling, allow default group to be updated alongside substates
580		if (v == "default") then
581			dstid = shdr.shid;
582			dstuni = shdr.uniforms;
583		else
584			if (shdr.states[v]) then
585				dstid = shdr.states[v].shid;
586				dstuni = shdr.states[v].uniforms;
587			end
588		end
589-- update the current "default" if this is set, in order to implement
590-- uniform persistance across restarts
591		if (dstid) then
592			if (set_uniform(dstid, uname, shdr.uniforms[uname].utype,
593				args, "update_uniform-" .. sname .. "-"..uname) and dstuni[uname]) then
594				dstuni[uname].default = args;
595			end
596		end
597	end
598end
599
600function shader_getkey(name, domain)
601	if (not domain) then
602		domain = groups;
603	end
604
605	if (type(domain) ~= "table") then
606		domain = {domain};
607	end
608
609-- the end- slide of Lua, why u no continue ..
610	for i,j in ipairs(domain) do
611		if (shdrtbl[j]) then
612			for k,v in pairs(shdrtbl[j]) do
613				if (v.label == name or k == name) then
614					return k, j;
615				end
616			end
617		end
618	end
619end
620
621function shader_key(label, domain)
622	for k,v in ipairs(shdrtbl[domain]) do
623		if (v.label == label) then
624			return k;
625 		end
626 	end
627end
628
629function shader_list(domain)
630	local res = {};
631
632	if (type(domain) ~= "table") then
633		domain = {domain};
634	end
635
636	for i,j in ipairs(domain) do
637		if (shdrtbl[j]) then
638			for k,v in pairs(shdrtbl[j]) do
639				table.insert(res, v.label);
640			end
641		end
642	end
643	return res;
644end
645