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