1-- Copyright: 2015-2020, Björn Ståhl
2-- License: 3-Clause BSD
3-- Reference: http://durden.arcan-fe.com
4--
5-- Description: The display- set of functions tracks connected displays
6-- and respond to plug/unplug events. They are also responsible for the
7-- creation of tiler- window managers and manual or automatic migration
8-- between window managers and their corresponding display.
9--
10
11-- default PPCM, particularly some capture devices that tamper with EDID.
12if VPPCM > 240 then
13	VPPCM = 32
14end
15
16local SIZE_UNIT = 38.4;
17local displays = {};
18local is_display_simple = false;
19local display_main = 1;
20
21local profiles = {};
22local ignored = {};
23local display_listeners = {};
24
25-- there's no other way to detect the presence of LWA mode at the moment
26-- (which is just stupid since some functions have different semantics)
27local arcan_nested = VRES_AUTORES ~= nil;
28
29-- set on display_init, used to slowly refactor / decouple display.lua from
30-- durden so that it can be added as a built-in instead
31local wm_alloc_function;
32local display_event_buffer = {};
33local set_view_range;
34local display_log, fmt = suppl_add_logfn("display");
35
36local function disp_string(disp)
37	return string.format("id=%d:name=%s:maphint=%d:w=%d:h=%d:backlight=%d:ppcm=%f",
38		disp.id and disp.id or -1, disp.name and disp.name or "broken",
39		disp.maphint and disp.maphint or -1,
40		disp.w and disp.w or -1,
41		disp.h and disp.h or -1,
42		disp.backlight and disp.backlight or -1,
43		disp.ppcm and disp.ppcm or -1
44	);
45end
46
47-- always return a valid string, for debug log tagging
48local function get_disp_name(name)
49	if (type(name) == "string") then
50		return name;
51	elseif (type(name) == "number") then
52		return tostring(number);
53	else
54		return "invalid";
55	end
56end
57
58local function get_disp(name)
59	local found, foundi;
60	for k,v in ipairs(displays) do
61		if (type(name) == "string" and v.name == name) then
62			found = v;
63			foundi = k;
64			break;
65		elseif (type(name) == "number" and v.id == name) then
66			found = v;
67			foundi = k;
68		end
69	end
70	return found, foundi;
71end
72
73local function tryload(v)
74	local res = system_load(v, 0);
75
76	if (not res) then
77		warning("parsing error loading display map: " .. v);
78		return;
79	end
80
81	local okstate, map = pcall(res);
82	if (not okstate or type(map) ~= "table") then
83		warning("execution error loading map: " .. v);
84		return;
85	end
86
87	if (type(map.name) ~= "string" or
88		type(map.ident) ~= "string") then
89		warning("bad obligatory fields in map: " .. v);
90		return;
91	end
92
93	local rv = {
94		name = map.name,
95		ident = map.ident,
96		tag = map.tag
97	};
98
99-- copy and sanity check optional fields
100
101	if (type(map.ppcm) == "number" and map.ppcm < 200 and map.ppcm > 10) then
102		rv.ppcm = map.ppcm;
103	end
104
105	if (type(map.wm) == "string") then
106		if (map.wm == "tiler" or map.wm == "ignore") then
107			rv.wm = map.wm;
108		end
109	end
110
111	if (type(map.backlight) == "number" and map.backlight > 0.0) then
112		rv.backlight = map.backlight;
113	end
114
115	if (type(map.width) == "number" and map.width > 0) then
116		rv.width = map.width;
117	end
118
119	if (type(map.height) == "number" and map.height > 0) then
120		rv.height = map.height;
121	end
122
123	return rv;
124end
125
126function display_scanprofiles()
127	profiles = {};
128	local lst = glob_resource("devmaps/display/*.lua", APPL_RESOURCE);
129	if (not lst) then
130		return;
131	end
132	table.sort(lst);
133	for _,v in ipairs(lst) do
134		local res = tryload("devmaps/display/" .. v);
135		if (res) then
136			table.insert(profiles, res);
137		end
138	end
139end
140
141display_scanprofiles();
142
143function display_maphint(disp)
144	if (type(disp) == "string") then
145		disp = get_disp(disp);
146	end
147
148	if (type(disp) ~= "table") then
149		return HINT_NONE;
150	end
151
152	return bit.bor(disp.maphint, (disp.primary and HINT_PRIMARY or 0));
153end
154
155local function autohome_windows(ndisp)
156
157-- find all windows that belong to this display, and move them back
158	for wnd in all_displays_iter() do
159		if wnd.adopt_display and wnd.adopt_display.ws_home == ndisp.name then
160			wnd:migrate(ndisp.tiler, ndisp);
161			if wnd.adopt_display.index then
162				wnd:reassign(wnd.adopt_display.index);
163			end
164
165			wnd.adopt_display = nil;
166		end
167	end
168
169	for _,disp in ipairs(displays) do
170		local tiler = disp.tiler;
171
172		if (tiler and tiler ~= ndisp.tiler) then
173			for i=1,10 do
174				if (tiler.spaces[i] and tiler.spaces[i].home and
175					tiler.spaces[i].home == ndisp.name) then
176					tiler.spaces[i]:migrate(ndisp.tiler);
177					display_log(fmt("autohome:%s:%d:%s", tiler.name, i, ndisp.name));
178				end
179			end
180		end
181	end
182
183-- there might be individual windows after a 'reset' that
184end
185
186local function set_mouse_scalef()
187	local sf = gconfig_get("mouse_scalef");
188	mouse_cursor_sf(sf * displays[display_main].tiler.scalef,
189		sf * displays[display_main].tiler.scalef);
190end
191
192-- execute [cb] in the attachment context of [tiler], needed with
193-- rendertargets as images created has a default attachment unless
194-- one is explicitly set
195function display_tiler_action(tiler, cb)
196	for i,v in ipairs(displays) do
197		if (v.tiler == tiler) then
198			local save = display_main;
199			set_context_attachment(v.rt);
200			cb();
201			set_context_attachment(displays[save].rt);
202			return;
203		end
204	end
205end
206
207-- same as for display_tiler_action, just a different lookup function
208function display_action(disp, cb)
209	local save = display_main;
210
211	if (type(disp) == "number") then
212		set_context_attachment(disp);
213	else
214		set_context_attachment(disp.rt);
215	end
216	cb();
217	set_context_attachment(displays[save].rt);
218end
219
220local function switch_active_display(ind)
221	if (displays[ind] == nil or not
222		valid_vid(displays[ind].rt) or type(ind) 	~= "number") then
223		return;
224	end
225
226	displays[display_main].tiler:deactivate();
227	displays[ind].tiler:activate();
228	display_main = ind;
229	set_context_attachment(displays[ind].rt);
230	mouse_querytarget(displays[ind].rt);
231	display_log(fmt("active_display:ind=%d:id=%d", ind, displays[ind].id));
232	set_mouse_scalef();
233end
234
235function display_output_table(name)
236	local disp
237	local outtbl = {
238		width = VRESH,
239		height = VRESW
240	}
241
242	if not name then
243		disp = displays[display_main];
244	else
245		disp = get_disp(name);
246	end
247
248	if disp then
249		outtbl.width = disp.w
250		outtbl.height = disp.h
251		outtbl.refresh = disp.refresh
252	end
253
254	return outtbl;
255end
256
257local function set_best_mode(disp, desw, desh)
258-- fixme, enumerate list of modes and pick one that has a fitting
259-- resolution and refresh
260	local list = video_displaymodes(disp.id);
261	if (not list or #list == 0) then
262		display_log(fmt(
263			"mode_error:message=no_mode:display=%s", tostring(disp.id)));
264		return;
265	end
266
267	if (not desw or not desh) then
268		desw = disp.w;
269		desh = disp.h;
270	end
271
272-- just score based on match against w/h
273	table.sort(list, function(a, b)
274		local dx = desw - a.width;
275		local dy = desh - a.height;
276		local ea = math.sqrt((dx * dx) + (dy * dy));
277		dx = desw - b.width;
278		dy = desh - b.height;
279		local eb = math.sqrt((dx * dx) + (dy * dy));
280
281-- same resolution? take the matching refresh, not the highest as that would
282-- excluding have a device- profile override
283		if (ea == eb) then
284			return math.abs(disp.refresh - a.refresh) > math.abs(disp.refresh - b.refresh);
285		end
286
287		return ea < eb;
288	end);
289
290	display_log(
291		fmt("mode_set:display=%d:width=%d:height=%d:refresh=%d",
292		disp.id, list[1].width, list[1].height, list[1].refresh)
293	);
294
295	video_displaymodes(disp.id, list[1].modeid);
296end
297
298local function get_ppcm(pw_cm, ph_cm, dw, dh)
299	return (math.sqrt(dw * dw + dh * dh) /
300		math.sqrt(pw_cm * pw_cm + ph_cm * ph_cm));
301end
302
303function display_count()
304	return #displays;
305end
306
307-- "hard" fullscreen- mode where the window canvas is mapped directly to
308-- the display without going through the detour of a rendertarget. Note that
309-- this is not as close as we can go yet, but requires more platform support
310-- and loses the ability to apply a shader.
311--
312-- The 'real' version would require not only a mode-switch but:
313--
314--  * track producer state and mark that we need a scanout capable buffer
315--    (which depend on the buffer format and so on) handle and wrap- shmif
316--    vbuf-only drawing into such a buffer. This can already
317--
318--  * for kms-, have arcan directly wrap the shmif- part into a DMAbuf and
319--    send that as the scanout for cases.
320--
321--  * use more native post-processing for ICC-/ gamma correction
322--
323function display_fullscreen(name, vid, modesw)
324	local disp = get_disp(name);
325	if (not disp) then
326		return;
327	end
328
329-- invalid vid == switch back, do so by reactivating rendertarget
330-- updates and possible switch back to the last known mode
331	if not valid_vid(vid) then
332		display_log("fullscreen:off");
333
334		for i, j in ipairs(displays) do
335			if (valid_vid(j.rt)) then
336				rendertarget_forceupdate(j.rt, -1);
337			end
338		end
339
340		map_video_display(disp.map_rt, disp.id, display_maphint(disp));
341		if (disp.last_m and disp.fs_modesw) then
342			video_displaymodes(disp.id, disp.last_m.modeid);
343		end
344
345		disp.monitor_vid = nil;
346		disp.monitor_sprops = nil;
347
348		if (disp.fs_mode) then
349			local ws = disp.tiler.spaces[disp.tiler.space_ind];
350			if (type(ws[disp.fs_mode]) == "function") then
351				ws[disp.fs_mode](ws);
352			end
353			disp.fs_mode = nil;
354		end
355
356-- otherwise enter fullscreen, switch each rendertarget to a configurable
357-- 'in background' refresh rate to permit other effects etc. to be running
358-- but at a lower rate than the focused vid
359	else
360		display_log(fmt("fullscreen:%d", disp.id));
361		for i,j in ipairs(displays) do
362			if (valid_vid(j.rt)) then
363				rendertarget_forceupdate(j.rt, gconfig_get("display_fs_rtrate"));
364			end
365		end
366
367		disp.monitor_vid = vid;
368		local ws = disp.tiler.spaces[disp.tiler.space_ind];
369		disp.fs_mode = ws.mode;
370		map_video_display(vid, disp.id, display_maphint(disp));
371	end
372
373-- will be applied in tick as we don't know what render state we are called from
374	disp.fs_modesw = modesw;
375end
376
377local function find_profile(name)
378	for k,v in ipairs(profiles) do
379		if (string.match(name, v.ident)) then
380			return v;
381		end
382	end
383end
384
385-- parse and decode display information from the edid block, possible that
386-- we should allow an edid override here as well - though linux etc. provide
387-- that at a lower level
388local function display_data(id)
389	local data = video_displaydescr(id);
390	local model = "unknown";
391	local serial = "unknown";
392
393	if (not data) then
394		display_log(fmt("edid_fail:id=" .. tostring(id)));
395		return;
396	end
397
398-- data should typically be EDID, if it is 128 bytes long we assume it is
399	if (string.len(data) == 128 or string.len(data) == 256) then
400		for i,ofs in ipairs({54, 72, 90, 108}) do
401
402			if (string.byte(data, ofs+1) == 0x00 and
403			string.byte(data, ofs+2) == 0x00 and
404			string.byte(data, ofs+3) == 0x00) then
405				if (string.byte(data, ofs+4) == 0xff) then
406					serial = string.sub(data, ofs+5, ofs+5+12);
407				elseif (string.byte(data, ofs+4) == 0xfc) then
408					model = string.sub(data, ofs+5, ofs+5+12);
409				end
410			end
411
412		end
413	end
414
415	local strip = function(s)
416		local outs = {};
417		local len = string.len(s);
418		for i=1,len do
419			local ch = string.sub(s, i, i);
420			if string.match(ch, '[a-zA-Z0-9]') then
421				table.insert(outs, ch);
422			end
423		end
424		return table.concat(outs, "");
425	end
426
427	return strip(model), strip(serial);
428end
429
430local function get_name(id)
431	local name;
432	local ok = false;
433
434-- start with some fail-safe name
435	if (id == 0) then
436		name = "default_";
437	else
438-- first mapping nonsense has previously made it easier (?!)
439-- getting a valid EDID in some cases, might need to move this
440-- workaround to the platform layer though
441		name = "unknown_" .. tostring(id);
442		map_video_display(displays[1].map_rt, id, HINT_NONE);
443	end
444
445	local model, serial = display_data(id)
446
447-- we don't always get a model/serial from the EDID, but when we do:
448	if (model) then
449		name = string.split(model, '\r')[1] .. "/" .. serial
450		display_log(fmt("id=%s:model=%s:serial=%s", tostring(id), model, serial));
451
452-- now the display might already exist (do nothing), there might be a display
453-- with the same name/serial from a sloppy monitor (need to suffix)
454		for _, v in ipairs(displays) do
455			if v.name == name and not v.orphan and id ~= v.id then
456				name = name .. "_" .. tostring(id)
457				break
458			end
459		end
460		ok = true;
461	else
462		display_log(fmt("id=%d:error=no_edid", id));
463	end
464
465	return name, ok;
466end
467
468local function display_byname(name, id, w, h, ppcm)
469	local name, got_name = get_name(id);
470
471-- EDID resolving might have failed for some reason, if we have an orphan
472-- display and no other - chances are there was some bug from power
473-- save/suspend type action. In that case, just 'assume' the display from
474-- the previously known ID.
475	local res = {
476		w = w,
477		h = h,
478		rw = w,
479		rh = h,
480		ppcm = ppcm,
481		id = id,
482		name = name,
483		shader = gconfig_get("display_shader"),
484		maphint = HINT_NONE,
485		refresh = 60,
486		backlight = 1.0,
487		view_range = set_view_range,
488		zoom = {
489			level = 1.0,
490			x = 0,
491			y = 0
492		},
493		wm = "tiler"
494	};
495
496	local pref = "disp_" .. string.hexenc(res.name) .. "_";
497	local keys = match_keys(pref .. "%");
498
499	for i,v in ipairs(keys) do
500		local ind = string.find(v, "=");
501		if (ind) then
502			local key = string.sub(string.sub(v, 1, ind-1), string.len(pref) + 1);
503			local val = string.sub(v, ind+1);
504			if (key == "ppcm") then
505				if (tonumber(val)) then
506					res.ppcm = tonumber(val);
507				end
508			elseif (key == "map") then
509				if (tonumber(val)) then
510					res.maphint = tonumber(val);
511				end
512			elseif (key == "shader") then
513				res.shader = val;
514			elseif (key == "bg") then
515				res.bg = val;
516			elseif (key == "primary") then
517				res.primary = tonumber(val) == 1;
518			elseif (key == "w") then
519				res.w = tonumber(val);
520			elseif (key == "h") then
521				res.h = tonumber(val);
522			elseif (key == "refresh") then
523				res.refresh = tonumber(val);
524			elseif (key == "backlight") then
525				res.backlight = tonumber(val);
526			else
527				warning("unknown stored display setting with key " .. key);
528			end
529		end
530	end
531
532-- profile takes precedence over cached database key
533	local prof = find_profile(name);
534	if (prof) then
535		if (prof.width) then
536			res.w = prof.width;
537		end
538		if (prof.height) then
539			res.h = prof.height;
540		end
541		if (prof.refresh) then
542			res.refresh = prof.refresh;
543		end
544		if (prof.ppcm) then
545			res.ppcm = prof.ppcm;
546			res.ppcm_override = prof.ppcm;
547		end
548		if (prof.backlight) then
549			res.backlight = math.clamp(prof.backlight, 0.1, 1.0);
550		end
551		res.tag = prof.tag;
552		res.wm = prof.wm;
553	end
554
555-- distinguish between real-width and effective-width (rotation)
556	res.rw = res.w;
557	res.rh = res.h;
558	return res;
559end
560
561-- assume that we somehow lost state and have a valid display, rebuild it
562-- with the contents of the provided table
563local function display_apply(display)
564	display_override_density(display.name, display.ppcm);
565	display_reorient(display.name, display.maphint);
566	display_shader(display.name, display.shader);
567
568	if (display.bg and display.tiler) then
569		display.tiler:set_background(bg);
570	end
571end
572
573function display_manager_shutdown()
574	local ktbl = {};
575
576	for i,v in ipairs(displays) do
577		local pref = "disp_" .. string.hexenc(v.name) .. "_";
578
579		if (v.ppcm_override) then
580			ktbl[pref .. "ppcm"] = v.ppcm;
581		end
582		if (v.maphint) then
583			ktbl[pref .. "map"] = v.maphint;
584		end
585		if (v.shader) then
586			ktbl[pref .. "shader"] = v.shader;
587-- MISSING: pack/unpack shader arguments
588		end
589		if (v.backlight) then
590			ktbl[pref .. "backlight"] = v.backlight;
591		end
592		ktbl[pref .. "bg"] = v.background and v.background or "";
593
594		if (v.rw) then
595			ktbl[pref .. "w"] = v.rw;
596		end
597		if (v.rh) then
598			ktbl[pref .. "h"] = v.rh;
599		end
600		if (v.refresh) then
601			ktbl[pref .. "refresh"] = v.refresh;
602		end
603		ktbl[pref .. "primary"] = v.primary and 1 or 0;
604	end
605	store_key(ktbl);
606end
607
608local function reorient_ddisp(disp, hint)
609-- is an explicit map hint set? then toggle the bits but preserve
610-- other ones like CROP or FILL or PRIMARY
611	local mfl = bit.bor(HINT_ROTATE_CW_90, HINT_ROTATE_CCW_90);
612	if (hint ~= nil) then
613		local valid = bit.bor(HINT_ROTATE_CW_90, HINT_ROTATE_CCW_90);
614		valid = bit.bor(valid, HINT_YFLIP);
615		hint = bit.band(valid, hint);
616		local mask = bit.band(disp.maphint, bit.bnot(valid));
617		disp.maphint = bit.bor(mask, hint);
618
619-- otherwise invert the current one
620	else
621		if (bit.band(disp.maphint, mfl) > 0) then
622			disp.maphint = bit.band(disp.maphint, bit.bnot(mfl));
623		else
624			disp.maphint = bit.bor(disp.maphint, HINT_ROTATE_CW_90);
625		end
626	end
627
628	local neww = disp.rw;
629	local newh = disp.rh;
630	if (bit.band(disp.maphint, mfl) > 0) then
631		neww = disp.rh;
632		newh = disp.rw;
633	end
634
635-- if the dimensions have changed, we should tell the tilers to reorg.
636	if (neww ~= disp.w or newh ~= disp.h) then
637		disp.w = neww;
638		disp.h = newh;
639		display_action(disp, function()
640			disp.tiler:resize(neww, newh);
641			disp.tiler:update_scalef(disp.tiler.scalef);
642		end);
643
644-- and this might have rebuilt the rendertarget as a new one, so switch
645-- the query target that the mouse will check for picking on
646		if (active_display(true) == disp.rt) then
647			mouse_querytarget(disp.rt);
648		end
649	end
650
651	map_video_display(disp.map_rt, disp.id, display_maphint(disp));
652end
653
654function display_set_backlight(name, ctrl, ind)
655	local disp = get_disp(name);
656	if (not disp) then
657		return;
658	end
659
660	if not (ctrl and ctrl >= 0 and ind and ind >= 0) then
661		disp.ledctrl = nil;
662		disp.ledid = nil;
663		return;
664	end
665
666	disp.ledctrl = ctrl;
667	disp.ledid = ind;
668	led_intensity(ctrl, ind, 255.0 * disp.backlight);
669end
670
671local function modestr(tbl)
672	return string.format(
673		"%d*%d (%.2f+%.2f mm) @ %.2f hz",
674			tbl.width, tbl.height, tbl.phy_width_mm, tbl.phy_height_mm, tbl.refresh);
675end
676
677local function display_added(id)
678	local modes = video_displaymodes(id);
679
680-- "safe" defaults
681	local dw = VRESW;
682	local dh = VRESH;
683	local ppcm = VPPCM;
684	local subpx = "RGB";
685
686-- map resolved display modes, assume [1] is the preferred one
687	if (modes and #modes > 0 and modes[1].width > 0) then
688		for i,v in ipairs(modes) do
689			display_log(fmt(
690				"status=modedata:id=%s:mode=%d:modestr=%s", id, i, modestr(v)));
691		end
692
693		dw = modes[1].width;
694		dh = modes[1].height;
695		local wmm = modes[1].phy_width_mm;
696		local hmm = modes[1].phy_height_mm;
697
698		subpx = modes[1].subpixel_layout;
699		subpx = subpx == "unknown" and "RGB" or subpx;
700
701		if (wmm > 0 and hmm > 0) then
702			ppcm = get_ppcm(0.1*wmm, 0.1*hmm, dw, dh);
703		end
704	else
705		display_log(fmt("status=error:id=%d:message=no modes on display", id));
706	end
707
708	local ddisp;
709
710-- if this 'fails', name will be some generated default as we know we have a
711-- new display we just wasn't able to extract it from EDID because
712-- all-identifiers-are-broken-fsck-hardware(TM), so as a mitigation to those
713-- bugs, pick a pre-existing display with the same id IF it is orphaned,
714-- otherwise the first known orphaned display.
715--
716-- A softer version for this might also be useful by throwing in a timer so
717-- that we first map something to the display, give it enough time to
718-- propagate, and then re-query EDID.
719	local name, name_ok = get_name(id);
720	if not name_ok then
721		local disp = get_disp(id);
722		if disp.orphan then
723			name = disp.name
724			display_log(fmt("id=%d:status=warning:fail_assume_same_adopt:name=%s", id, name));
725		else
726			for i,v in ipairs(displays) do
727				if (v.orphan) then
728					name = v.name
729					display_log(fmt("id=%d:status=warning:fail_assume_adopt:name=%s", i, name));
730					break
731				end
732			end
733		end
734	end
735
736	ddisp = display_add(name, dw, dh, ppcm, id);
737	if (not ddisp) then
738		display_log(fmt("status=error:id=%d:message=display_add failed", id));
739		return;
740	end
741
742	ddisp.id = id;
743
744-- load possible overrides since before, note that this is slightly
745-- inefficient as it will force rebuild of underlying rendertargets
746-- etc. but it beats have to cover a number of corner cases / races
747	ddisp.ppcm = ppcm;
748	ddisp.subpx = subpx;
749
750-- get the current state of the color ramps and attach to the disp-
751-- table, for both internal and external 'gamma' correction.
752	if (not ddisp.ramps) then
753		ddisp.ramps = video_displaygamma(ddisp.id);
754		ddisp.active_ramps = ddisp.ramps;
755	end
756
757	display_log(disp_string(ddisp));
758	display_apply(ddisp);
759	map_video_display(ddisp.map_rt, id, display_maphint(ddisp));
760	if (ddisp.bg) then
761		ddisp.tiler:set_background(ddisp.bg);
762	end
763	for k,v in ipairs(display_listeners) do
764		v("added", name, ddisp.tiler, id);
765	end
766end
767
768local last_rescan = CLOCK;
769function display_event_handler(action, id)
770	if not wm_alloc_function then
771		table.insert(display_event_buffer, {action, id});
772		return;
773	end
774
775	display_log(fmt("id=%d:event=%s",
776		id and id or -1, action and action or ""));
777
778-- display subsystem and input subsystem are connected when it comes
779-- to platform specific actions e.g. virtual terminal switching, assume
780-- keystate change between display resets.
781	if (action == "reset") then
782		dispatch_meta_reset();
783		iostatem_reset_flag();
784		return;
785	end
786
787	if (action == "added") then
788		display_added(id);
789
790-- remove on a previous display is more like tagging it as orphan
791-- as it may reappear later
792	elseif (action == "removed") then
793		local ddisp = display_remove(id);
794		if (ddisp) then
795			for k,v in ipairs(display_listeners) do
796				v("removed", name, ddisp.tiler, id);
797			end
798		end
799
800	elseif (action == "changed") then
801		active_display():message("rescanning GPUs on hotlug");
802
803-- prevent hotplug storms (such as plugging in a dock or kvm switch)
804-- from causing multiple rescans and the stalls that entail
805		if (last_rescan ~= CLOCK) then
806			video_displaymodes();
807			last_rescan = CLOCK;
808		end
809	end
810end
811
812--
813-- a facility to monitor when a display is added or lost as some
814-- global effects need to know about this in order to build fbos etc.
815--
816function display_add_listener(fcon)
817	table.insert(display_listeners, fcon);
818	for i,v in ipairs(displays) do
819		fcon("added", v.name, v.tiler, v.id);
820	end
821end
822
823function display_all_mode(mode)
824	for i,v in ipairs(displays) do
825		video_display_state(v.id, mode);
826	end
827end
828
829set_view_range =
830function(disp, x, y, factor)
831	disp.zoom.level = math.clamp(factor, 1.0, 100.0);
832
833-- just normal mapping again
834	if disp.zoom.level == 1.0 then
835		image_set_txcos_default(disp.rt);
836		map_video_display(disp.map_rt, disp.id, display_maphint(disp));
837		return;
838	end
839
840	disp.zoom.x = math.clamp(x, 0.0, 1.0);
841	disp.zoom.y = math.clamp(y, 0.0, 1.0);
842
843-- just uniform
844	local base = 1.0 / factor;
845	local s1 = disp.zoom.x * base;
846	local t1 = disp.zoom.y * base;
847	local s2 = s1 + base;
848	local t2 = t1 + base;
849
850-- optional step, align against sampling grid s1 - (math.fmod s1, base)
851
852-- clamp against edge
853	if (s2 > 1.0) then
854		s1 = s1 - (s2 - 1.0);
855		s2 = 1.0;
856	end
857
858	if (t2 > 1.0) then
859		t1 = t1 - (t2 - 1.0);
860		t2 = 1.0;
861	end
862
863-- and synch
864	local txcos = {s1, t1, s2, t1, s2, t2, s1, t2};
865	image_set_txcos(disp.rt, txcos);
866	map_video_display(disp.map_rt, disp.id, display_maphint(disp));
867end
868
869function display_manager_init(alloc_fn)
870	wm_alloc_function = alloc_fn;
871
872-- Since we won't get an 'added / removed' event for this display, the defaults
873-- and possible profile override needs to be activated manually. This should
874-- really be reworked to have the same path for everything, the problem lies
875-- with how the platform is setup in Arcan, which in turn ties back to openGL
876-- setup without a working display.
877	local name = get_name(0);
878	local ddisp = display_byname(name, 0, VRESW, VRESH, VPPCM);
879
880-- this might come from a preset profile, so sweep the available display maps
881-- and pick the one with the best fit
882	set_best_mode(ddisp);
883
884-- virtual-display to-fix: there is an issue here when the system starts
885-- without any connected display or when the first display happens to be
886-- a VR display that should be ignored. What should be done is to create
887-- a virtual-display and bind a tiler to that, then allow a display to
888-- grab the orphaned virtual one.
889	ddisp.tiler = wm_alloc_function(ddisp);
890
891	displays[1] = ddisp;
892
893	is_display_simple = gconfig_get("display_simple");
894	display_main = 1;
895	ddisp.ind = 1;
896	ddisp.tiler.name = "default";
897
898-- simple mode does not permit us to do much of the fun stuff, like
899-- different color etc. correction shaders or rotate/fit/.. it's
900-- essentially just for low powered nested use
901
902	if (not is_display_simple) then
903		rendertarget_forceupdate(WORLDID, 0);
904		if (not arcan_nested) then
905			delete_image(WORLDID);
906		end
907		ddisp.rt = ddisp.tiler:set_rendertarget(true);
908		ddisp.map_rt = ddisp.rt;
909
910		map_video_display(ddisp.map_rt, 0, 0);
911		shader_setup(ddisp.map_rt, "display", ddisp.shader, ddisp.name);
912		switch_active_display(1);
913		reorient_ddisp(ddisp, ddisp.maphint);
914		mouse_querytarget(ddisp.rt);
915	end
916
917-- any deferred events from display events arriving before the caller
918-- has called init gets re-injected, this can also be used to test some
919-- of the hotplug behaviors
920	for _,v in ipairs(display_event_buffer) do
921		display_event_handler(unpack(v))
922	end
923	display_event_buffer = {};
924
925	return ddisp.tiler;
926end
927
928function display_attachment()
929	if (not is_display_simple) then
930		return nil;
931	else
932		return displays[1].rt;
933	end
934end
935
936function display_override_density(name, ppcm)
937	local disp, dispi = get_disp(name);
938	if (not disp) then
939		return;
940	end
941
942-- it might be that the selected display is not currently the main one
943	display_action(disp, function()
944		disp.ppcm = ppcm;
945		disp.ppcm_override = ppcm;
946		disp.tiler:update_scalef(ppcm / SIZE_UNIT, {ppcm = ppcm});
947		set_mouse_scalef();
948	end);
949end
950
951-- override the default shader setting to packval, that can be expanded
952-- upon display identification and shader setup
953function display_shader_uniform(name, uniform, packval)
954--	print("update uniform persistance", name, uniform, packval);
955end
956
957function display_shader(name, key)
958	local disp, dispi = get_disp(name);
959	if (not disp or not valid_vid(disp.rt)) then
960		return;
961	end
962
963-- special path, the engine can optimize if we use the "DEFAULT" shader
964	if (key == "basic") then
965		image_shader(disp.rt, 'DEFAULT');
966		disp.shader = key;
967	elseif (key) then
968		warning("shader" .. key);
969		shader_setup(disp.rt, "display", key, disp.name);
970		--set_key("disp_" .. hexenc(disp.name) .. "_shader", key);
971		disp.shader = key;
972	end
973	map_video_display(disp.map_rt, disp.id, disp.maphint);
974
975	return disp.shader;
976end
977
978function display_add(name, width, height, ppcm, id)
979	local found = get_disp(name);
980	local new = nil;
981	local maphint = HINT_NONE;
982	local backlight = 1.0;
983
984	name = string.gsub(name, ":", "/");
985
986	width = math.clamp(width, width, MAX_SURFACEW);
987	height = math.clamp(height, height, MAX_SURFACEH);
988
989-- for each workspace, check if they are homed to the display
990-- being added, and, if space exists, migrate
991	if (found) then
992		display_log(fmt("add_match:name=%s", string.hexenc(name)));
993		found.orphan = false;
994		image_resize_storage(found.rt, found.w, found.h);
995		display_apply(found);
996
997	else
998		nd = display_byname(name, id, width, height, ppcm);
999		if (nd.wm == "ignore") then
1000			table.insert(ignored, nd);
1001			return;
1002		end
1003
1004-- make sure all resources are created in the global scope
1005		set_context_attachment(WORLDID);
1006		nd.tiler = wm_alloc_function(nd);
1007		table.insert(displays, nd);
1008		nd.ind = #displays;
1009		new = nd.tiler;
1010
1011-- this will rebuild tiler with all its little things attached to rt
1012-- we hide it as we explicitly map to a display and do not want it
1013-- visible in the WORLDID domain, eating fillrate.
1014		nd.rt = nd.tiler:set_rendertarget(true);
1015		nd.map_rt = nd.rt;
1016		hide_image(nd.rt);
1017
1018-- in the real case, we'd switch to the last known resolution
1019-- and then set the display to match the rendertarget
1020		found = nd;
1021		set_context_attachment(displays[display_main].rt);
1022	end
1023
1024-- this also takes care of spaces that are saved as preferring a certain disp.
1025	autohome_windows(found);
1026
1027	if (found.last_m) then
1028		display_ressw(name, found.last_m);
1029	end
1030	return found, new;
1031end
1032
1033-- linear search all spaces in all displays except disp and
1034-- return the first empty one that is found
1035local function find_free_display(disp)
1036	for i,v in ipairs(displays) do
1037		if (not v.orphan and v ~= disp) then
1038			for j=1,10 do
1039				if (v.tiler:empty_space(j)) then
1040					return v;
1041				end
1042			end
1043		end
1044	end
1045end
1046
1047-- sweep all used workspaces of the display and find new parents
1048local function autoadopt_display(disp)
1049	local set = {}
1050	for i=1,10 do
1051		if not disp.tiler:empty_space(i) then
1052			table.insert(set, i)
1053		end
1054	end
1055
1056	if #set == 0 then
1057		return
1058	end
1059
1060	display_log(fmt(
1061		"attempt_adopt:orphans=%d:source=%d:name=%s", #set, disp.id, disp.name));
1062
1063	for _,v in ipairs(set) do
1064		local ddisp = find_free_display(disp);
1065
1066-- chances are all displays are lost
1067		if (not ddisp) then
1068			display_log("adopt_cancelled:no_free_display");
1069			return;
1070		end
1071
1072-- perform the migration, but remember the display
1073		local space = disp.tiler.spaces[v];
1074		if (space) then
1075			display_log(fmt("migrate:home=%s:dst=%s:index=%d", disp.name, ddisp.name, v));
1076			space:migrate(ddisp.tiler, ddisp);
1077			space.home = disp.name;
1078			space.home_index = v;
1079		else
1080			display_log("adopt_cancelled:space_empty");
1081		end
1082	end
1083end
1084
1085-- allow external tools to register ignored devices by tag
1086function display_bytag(tag, yield)
1087	for _,v in ipairs(ignored) do
1088		if (v.tag == tag and not v.leased) then
1089			yield(v);
1090		end
1091	end
1092end
1093
1094function display_lease(name)
1095	display_log(fmt("lease:name=%s", get_disp_name(name)));
1096
1097	for k,v in ipairs(ignored) do
1098		if (v.name == name) then
1099			if (not v.leased) then
1100				display_log(fmt("leased:name=%s", get_disp_name(name)));
1101				v.leased = true;
1102				return v;
1103			else
1104				display_log(fmt("lease_error:name=%s",get_disp_name(name)));
1105			end
1106		end
1107	end
1108
1109end
1110
1111function display_release(name)
1112	display_log(fmt("release:name=%s", get_disp_name(name)));
1113
1114	for k,v in ipairs(ignored) do
1115		if (v.name == name) then
1116			if (v.leased) then
1117				display_log(fmt("released:name=%s", get_disp_name(name)));
1118				v.leased = false;
1119				return;
1120			else
1121				display_log(fmt(("release_error:name=" .. get_disp_name(name))));
1122			end
1123		end
1124	end
1125end
1126
1127function display_remove_add(id)
1128	local found, foundi = get_disp(id)
1129	if not found then
1130		return
1131	end
1132	display_remove(id)
1133	display_added(id)
1134end
1135
1136function display_remove(id)
1137	local found, foundi = get_disp(id);
1138
1139-- there is still the chance that some other tool manually managed the
1140-- display, this is used in the case of a VR modelviewer, for instance.
1141	if (not found) then
1142
1143		for i,v in ipairs(ignored) do
1144			if v.id == id then
1145				if (v.handler) then
1146					display_log(fmt("remove_masked:id=%d", id));
1147					v:handler("remove");
1148				end
1149				table.remove(ignored,i);
1150				return;
1151			end
1152		end
1153
1154		display_log(fmt("remove:error:unknown=%s", get_disp_name(name)));
1155		return;
1156	end
1157
1158-- mark as orphan and reduce memory footprint by resizing the rendertarget
1159	display_log(fmt("orphan:id=%d:name=%s", id, get_disp_name(name)));
1160	found.orphan = true;
1161	image_resize_storage(found.rt, 32, 32);
1162	hide_image(found.rt);
1163
1164-- try and have another display adopt
1165	if (gconfig_get("ws_autoadopt") and autoadopt_display(found)) then
1166		found.orphan = false;
1167	end
1168
1169-- if it was the main display we lost, cycle to the next one so that gets
1170-- set as main
1171	if (foundi == display_main) then
1172		display_cycle_active(ws);
1173	end
1174
1175	return found;
1176end
1177
1178-- special little hook in LWA mode that handles resize requests from
1179-- parent. We treat that as a 'normal' resolution switch.
1180function VRES_AUTORES(w, h, vppcm, flags, source)
1181	local disp = displays[1];
1182	display_log(fmt(
1183		"autores:id=0:w=%d:h=%d:ppcm=%f:flags=%d:source=%d",
1184		w, h, vppcm, flags, source)
1185	);
1186
1187	for _,v in ipairs(displays) do
1188		if (v.id == source) then
1189			disp = v;
1190			break;
1191		end
1192	end
1193
1194	if (gconfig_get("lwa_autores")) then
1195		if (is_display_simple) then
1196			resize_video_canvas(w, h);
1197			disp.tiler:resize(w, h, true);
1198		else
1199			display_action(disp, function()
1200				if (video_displaymodes(source, w, h)) then
1201					map_video_display(disp.map_rt, 0, disp.maphint);
1202					resize_video_canvas(w, h);
1203					image_set_txcos_default(disp.rt);
1204					disp.tiler:resize(w, h);
1205					disp.tiler:update_scalef(disp.ppcm / SIZE_UNIT, {ppcm = disp.ppcm});
1206				end
1207			end);
1208		end
1209	end
1210end
1211
1212function display_ressw(name, mode)
1213	local disp = get_disp(name);
1214	if (not disp) then
1215		warning("display_ressww(), invalid display reference for "
1216			.. tostring(name));
1217		return;
1218	end
1219
1220-- track this so we can recover if the display is lost, readded and homed to def
1221	disp.last_m = mode;
1222
1223	if (not disp.ppcm_override) then
1224		disp.ppcm = get_ppcm(0.1 * mode.phy_width_mm,
1225			0.1 * mode.phy_height_mm, mode.width, mode.height);
1226	end
1227
1228	display_action(disp, function()
1229		disp.w = mode.width;
1230		disp.h = mode.height;
1231		disp.rw = disp.w;
1232		disp.rh = disp.h;
1233		video_displaymodes(disp.id, mode.modeid);
1234		if (valid_vid(disp.rt)) then
1235			image_set_txcos_default(disp.rt);
1236			map_video_display(disp.map_rt, disp.id, display_maphint(disp));
1237		end
1238		disp.tiler:resize(mode.width, mode.height) --, true);
1239		disp.tiler:update_scalef(disp.ppcm / SIZE_UNIT, {ppcm = disp.ppcm});
1240		set_mouse_scalef();
1241	end);
1242
1243	if (disp.maphint) then
1244		display_reorient(name, disp.maphint);
1245	end
1246
1247-- as the dimensions have changed
1248	if (active_display(true) == disp.rt) then
1249		mouse_querytarget(disp.rt);
1250	end
1251end
1252
1253function display_cycle_active(ind)
1254	if (type(ind) == "boolean") then
1255		switch_active_display(display_main);
1256		return;
1257	elseif (type(ind) == "number") then
1258		switch_active_display(ind);
1259		return;
1260	end
1261
1262	local nd = display_main;
1263	repeat
1264		nd = (nd + 1 > #displays) and 1 or (nd + 1);
1265	until (nd == display_main or not
1266		(displays[nd].orphan or displays[nd].disabled));
1267
1268	switch_active_display(nd);
1269end
1270
1271function display_migrate_wnd(wnd, dstname)
1272	local dsp2 = get_disp(dstname);
1273	if (not dsp2) then
1274		return;
1275	end
1276
1277	wnd:migrate(dsp2.tiler, {ppcm = dsp2.ppcm,
1278		width = dsp2.tiler.width, height = dsp2.tiler.height});
1279end
1280
1281-- migrate the ownership of a single workspace to another display
1282function display_migrate_ws(tiler, dstname)
1283	local dsp2 = get_disp(dstname);
1284	if (not dsp2) then
1285		return;
1286	end
1287
1288	if (#tiler.spaces[tiler.space_ind].children > 0) then
1289		tiler.spaces[tiler.space_ind]:migrate(dsp2.tiler,
1290			{ppcm = dsp2.ppcm,
1291			width = dsp2.tiler.width, height = dsp2.tiler.height
1292		});
1293		tiler:tile_update();
1294		dsp2.tiler:tile_update();
1295	end
1296end
1297
1298function display_reorient(name, hint)
1299	if (is_display_simple) then
1300		return;
1301	end
1302
1303	local disp = get_disp(name);
1304	if (not disp) then
1305		warning("display_reorient on missing display:" .. tostring(name));
1306		return;
1307	end
1308
1309	reorient_ddisp(disp, hint);
1310end
1311
1312function display_simple()
1313	return is_display_simple;
1314end
1315
1316function display_share(disp, args, recfn)
1317	if (not valid_vid(disp.rt)) then
1318		return;
1319	end
1320
1321	if (disp.share_slot) then
1322		delete_image(disp.share_slot);
1323		disp.share_slot = nil;
1324	else
1325-- this one can't handle resolution switching and we ignore audio for the
1326-- time being or we'd need to do a lot of attachment tracking
1327		local isp = image_storage_properties(disp.rt);
1328		disp.share_slot = alloc_surface(isp.width, isp.height, true);
1329		local indir = null_surface(isp.width, isp.height);
1330		show_image(indir);
1331		image_sharestorage(disp.rt, indir);
1332		define_recordtarget(disp.share_slot,
1333		recfn, args, {indir}, {}, RENDERTARGET_DETACH, RENDERTARGET_NOSCALE, -1,
1334		function(src, status)
1335		end
1336		);
1337	end
1338end
1339
1340-- the active displays is the rendertarget that will (initially) create new
1341-- windows, though they can be migrated immediately afterwards. This is because
1342-- both mouse_ implementation and new object attachment points are a global
1343-- state.
1344function active_display(rt, raw)
1345	if (raw) then
1346		return displays[display_main];
1347	end
1348
1349	if (not displays[display_main]) then
1350		return;
1351	end
1352
1353	if (rt) then
1354		return displays[display_main].rt;
1355	else
1356		return displays[display_main].tiler;
1357	end
1358end
1359
1360--
1361-- These iterators are primarily for archetype handlers and similar where we
1362-- need "all windows regardless of display".  Don't break- out of this or
1363-- things may get the wrong attachment later.
1364--
1365local function aditer(rawdisp, showorph, showdis)
1366	local tbl = {};
1367	for i,v in ipairs(displays) do
1368		if ((not v.orphan or showorph) and (not v.disabled or showdis)) then
1369			table.insert(tbl, {i, v});
1370		end
1371	end
1372	local c = #tbl;
1373	local i = 0;
1374	local save = display_main;
1375
1376	return function()
1377		i = i + 1;
1378		if (i <= c) then
1379			switch_active_display(tbl[i][1]);
1380			return rawdisp and tbl[i][2] or tbl[i][2].tiler;
1381		else
1382			switch_active_display(save);
1383			return nil;
1384		end
1385	end
1386end
1387
1388function all_tilers_iter()
1389	return aditer(false);
1390end
1391
1392function all_displays_iter()
1393	return aditer(true);
1394end
1395
1396function all_spaces_iter()
1397	local tbl = {};
1398	for i,v in ipairs(displays) do
1399		for k,l in pairs(v.tiler.spaces) do
1400			table.insert(tbl, {i,l});
1401		end
1402	end
1403	local c = #tbl;
1404	local i = 0;
1405	local save = display_main;
1406
1407	return function()
1408		i = i + 1;
1409		if (i <= c) then
1410			switch_active_display(tbl[i][1]);
1411			return tbl[i][2];
1412		else
1413			switch_active_display(save);
1414			return nil;
1415		end
1416	end
1417end
1418
1419function all_windows(atype, noswitch)
1420	local tbl = {};
1421	for i,v in ipairs(displays) do
1422		for j,k in ipairs(v.tiler.windows) do
1423			table.insert(tbl, {i, k});
1424		end
1425	end
1426
1427	local i = 0;
1428	local c = #tbl;
1429	local save = display_main;
1430
1431	return function()
1432		i = i + 1;
1433		while (i <= c) do
1434			if (not atype or (atype and tbl[i][2].atype == atype)) then
1435				if (not noswitch) then
1436					switch_active_display(tbl[i][1]);
1437				end
1438				return tbl[i][2];
1439			else
1440				i = i + 1;
1441			end
1442		end
1443		if (not noswitch) then
1444			switch_active_display(save);
1445		end
1446		return nil;
1447	end
1448end
1449
1450function displays_alive(filter)
1451	local res = {};
1452
1453	for k,v in ipairs(displays) do
1454		if (not (v.orphan or v.disabled) and (not filter or k ~= display_main)) then
1455			table.insert(res, v.name);
1456		end
1457	end
1458	return res;
1459end
1460
1461function display_tick()
1462	for k,v in ipairs(displays) do
1463		if (not v.orphan) then
1464			v.tiler:tick();
1465		end
1466
1467-- periodically check source for dedicated fullscreen mode
1468		if (not is_display_simple and v.monitor_vid) then
1469
1470-- on death, set "BADID" (which will revert mapping to normal rt)
1471			if (not valid_vid(v.monitor_vid, TYPE_FRAMESERVER)) then
1472				display_fullscreen(v.name, BADID);
1473			else
1474				local isp = image_storage_properties(v.monitor_vid);
1475
1476-- deferred resize- propagation due to cost of mode switch, this could probably
1477-- be even more conservative, though resolution switches in the source will
1478-- cause a visual glitch for the 'incorrect' frames.
1479				if (not v.monitor_sprops or isp.width ~= v.monitor_sprops.width or
1480					isp.height ~= v.monitor_sprops.height) then
1481					v.monitor_sprops = isp;
1482					if (v.fs_modesw) then
1483						set_best_mode(v, isp.width, isp.height);
1484					end
1485-- remap so crop-center works
1486				end
1487			end
1488		end
1489	end
1490end
1491