; SPDX-License-Identifier: NOASSERTION ; ; VMODE - the desktop size on a panel the firmware already drives ; ; The panel keeps the timing, the PLL and the encoder the firmware set up: a ; full modeset is the part that leaves a panel dark when it goes wrong. Only ; what the display engine reads out of the framebuffer, and how it scales that ; up to the panel, changes; then the kernel is told the new size through ; SetScreen, once the display engine already shows it. ; ; Intel Gen9 (Skylake, Apollo Lake, Gemini Lake): any size up to the panel ; whose lines fit the pitch the kernel draws with. The pipe scaler fits it ; to the panel in its own aspect, centred; the panel's own size turns the ; scaler off. ; ; AMD DCE4 (Evergreen, Northern Islands, the PALM/SUMO IGPs): the panel's ; height with a width up to the panel's. The surface widens and only the ; horizontal ratio of the scaler changes, the vertical half is left alone. ; ; The service VMODE: VMODE_GET_INFO says what the hardware allows, ; VMODE_SET_SIZE changes the size. Every test that can refuse runs before the ; first write, so a refusal leaves the hardware and the kernel as they were. ; Why a call refused is on the debug board. ; ==================================================================== format PE DLL native 0.05 entry START DEBUG = 1 __DEBUG__ = 1 __DEBUG_LEVEL__ = 1 section '.flat' code readable writable executable include '../proc32.inc' include '../struct.inc' include '../macros.inc' include '../peimport.inc' include '../fdo.inc' ; ==================================================================== proc START c, reason:dword, cmdline:dword cmp [reason], DRV_ENTRY jne .fail invoke GetDisplay mov ecx, [eax + DISPLAY_WIDTH] mov [info.firmware_w], ecx mov ecx, [eax + DISPLAY_HEIGHT] mov [info.firmware_h], ecx mov ecx, [eax + DISPLAY_PITCH] mov [lfb_pitch], ecx mov ecx, [eax + DISPLAY_BPP] shr ecx, 3 mov [bytes_pp], ecx call gen9_detect jnc .found call dce4_detect jnc .found DEBUGF 1, "vmode: no Gen9 or DCE4 display the firmware drives\n" jmp .fail .found: DEBUGF 1, "vmode: family %u, panel %ux%u, firmware %ux%u, max width %u\n", \ [info.family], [info.panel_w], [info.panel_h], \ [info.firmware_w], [info.firmware_h], [info.max_w] invoke RegService, service_name, service_proc ret .fail: xor eax, eax ret endp ; ==================================================================== proc service_proc stdcall, ioctl:dword mov edx, [ioctl] mov eax, [edx + IOCTL.io_code] cmp eax, SRV_GETVERSION jne .not_version cmp [edx + IOCTL.out_size], 4 jb .fail mov eax, [edx + IOCTL.output] mov dword [eax], API_VERSION jmp .done .not_version: cmp eax, VMODE_GET_INFO jne .not_info cmp [edx + IOCTL.out_size], sizeof.VMODE_INFO jb .fail push esi edi mov esi, info mov edi, [edx + IOCTL.output] mov ecx, sizeof.VMODE_INFO / 4 rep movsd pop edi esi jmp .done .not_info: cmp eax, VMODE_SET_SIZE jne .fail cmp [edx + IOCTL.inp_size], 8 jb .fail mov eax, [edx + IOCTL.input] mov ecx, [eax] mov [target_w], ecx mov ecx, [eax + 4] mov [target_h], ecx call set_size jc .fail .done: xor eax, eax ret .fail: or eax, -1 ret endp ; ==================================================================== ; target_w x target_h: the hardware, then the kernel; CF = 1: refused set_size: push ebx esi edi DEBUGF 1, "vmode: %ux%u asked\n", [target_w], [target_h] mov eax, [target_w] test eax, eax jz .refused cmp eax, [info.max_w] ja .too_wide mov eax, [target_h] test eax, eax jz .refused cmp eax, [info.panel_h] ja .too_high cmp [info.fixed_h], 0 je .family cmp eax, [info.panel_h] jne .not_panel_height .family: mov esi, [mmio] mov edi, [unit] cmp [info.family], FAMILY_GEN9 jne .dce4 call gen9_set_size jmp .kernel .dce4: call dce4_set_size jc .refused ; and only now the kernel, so that its redraw lands on a display engine that ; already shows the new size .kernel: invoke GetDisplay mov ecx, [target_w] mov [eax + DISPLAY_WIDTH], ecx mov ecx, [target_h] mov [eax + DISPLAY_HEIGHT], ecx mov eax, [target_w] dec eax mov edx, [target_h] dec edx mov ecx, [lfb_pitch] invoke SetScreen DEBUGF 1, "vmode: the kernel has %ux%u, pitch %u\n", \ [target_w], [target_h], [lfb_pitch] clc jmp .return .too_wide: DEBUGF 1, "vmode: wider than %u\n", [info.max_w] jmp .refused .too_high: DEBUGF 1, "vmode: higher than the panel, %u\n", [info.panel_h] jmp .refused .not_panel_height: DEBUGF 1, "vmode: the height must stay the panel's, %u\n", [info.panel_h] .refused: stc .return: pop edi esi ebx ret ; ==================================================================== ; Intel Gen9: the live pipe, its scaler and the panel into info; CF = 1: none gen9_detect: push ebx esi edi invoke PciRead32, 0, GEN9_DEVFN, PCI_VENDOR cmp ax, 0x8086 jne .none ; BAR0 is 64 bit; only the low 4G can be mapped invoke PciRead32, 0, GEN9_DEVFN, GEN9_BAR_HIGH test eax, eax jnz .above_4g invoke PciRead32, 0, GEN9_DEVFN, GEN9_BAR_LOW and eax, not 0xF invoke MapIoMem, eax, GEN9_MMIO_SIZE, MMIO_FLAGS test eax, eax jz .no_map mov esi, eax ; the live pipe is the one with a source size and an enabled plane; a block ; whose power well is down reads as all ones xor ebx, ebx xor edi, edi .pipe: mov eax, [esi + edi + GEN9_PIPESRC] test eax, eax jz .next_pipe cmp eax, -1 je .next_pipe test dword [esi + edi + GEN9_PLANE_CTL], GEN9_PLANE_ENABLE jnz .got_pipe .next_pipe: add edi, GEN9_PIPE_STEP inc ebx cmp ebx, GEN9_PIPES jb .pipe DEBUGF 1, "vmode: Gen9, no pipe drives a plane\n" jmp .unmap .got_pipe: ; the plane has to read with the pitch the kernel writes with mov eax, [esi + edi + GEN9_PLANE_STRIDE] and eax, GEN9_PLANE_STRIDE_MASK imul eax, GEN9_PLANE_STRIDE_UNIT cmp eax, [lfb_pitch] jne .pitch_mismatch ; whichever scaler the firmware bound to this pipe, otherwise the first: two ; scalers on one pipe is not a defined state. Pipe C has only one. mov ecx, ebx imul ecx, GEN9_PS_PIPE_STEP test dword [esi + ecx + GEN9_PS_CTRL], GEN9_PS_ENABLE jnz .have_scaler cmp ebx, GEN9_PIPES - 1 je .have_scaler test dword [esi + ecx + GEN9_PS_CTRL + GEN9_PS_STEP], GEN9_PS_ENABLE jz .have_scaler add ecx, GEN9_PS_STEP .have_scaler: mov [scaler], ecx ; a running scaler holds the panel as its window plus the margins it was ; centred with; without one the pipe drives the panel directly test dword [esi + ecx + GEN9_PS_CTRL], GEN9_PS_ENABLE jz .panel_from_pipe mov eax, [esi + ecx + GEN9_PS_WIN_SZ] mov edx, [esi + ecx + GEN9_PS_WIN_POS] movzx ecx, ax movzx ebx, dx lea ecx, [ecx + ebx*2] mov [info.panel_h], ecx shr eax, 16 shr edx, 16 lea eax, [eax + edx*2] mov [info.panel_w], eax jmp .have_panel .panel_from_pipe: mov eax, [esi + edi + GEN9_PIPESRC] movzx ecx, ax inc ecx mov [info.panel_h], ecx shr eax, 16 inc eax mov [info.panel_w], eax .have_panel: ; a line of any size has to fit the pitch the kernel goes on drawing with mov eax, [lfb_pitch] xor edx, edx div [bytes_pp] cmp eax, [info.panel_w] jbe .have_max mov eax, [info.panel_w] .have_max: mov [info.max_w], eax mov [info.fixed_h], 0 mov [info.family], FAMILY_GEN9 mov [mmio], esi mov [unit], edi clc jmp .return .pitch_mismatch: DEBUGF 1, "vmode: Gen9 plane stride %u is not the kernel pitch %u\n", \ eax, [lfb_pitch] .unmap: invoke FreeKernelSpace, esi jmp .none .above_4g: DEBUGF 1, "vmode: Gen9 registers are above 4G\n" jmp .none .no_map: DEBUGF 1, "vmode: cannot map the Gen9 registers\n" .none: stc .return: pop edi esi ebx ret ; ==================================================================== ; esi = registers, edi = the pipe; PIPE_SRC and PLANE_SIZE shrink to the ; target and the scaler fits it to the panel. The stride and the surface stay, ; so the framebuffer keeps the layout the kernel draws into. gen9_set_size: mov eax, [target_w] dec eax shl eax, 16 mov ecx, [target_h] dec ecx or eax, ecx mov [esi + edi + GEN9_PIPESRC], eax mov eax, [target_h] dec eax shl eax, 16 mov ecx, [target_w] dec ecx or eax, ecx mov [esi + edi + GEN9_PLANE_SIZE], eax ; the plane feeds the pipe whole and the scaler's window does the placing mov dword [esi + edi + GEN9_PLANE_POS], 0 mov ebx, [scaler] mov eax, [target_w] cmp eax, [info.panel_w] jne .scale mov eax, [target_h] cmp eax, [info.panel_h] jne .scale mov dword [esi + ebx + GEN9_PS_CTRL], 0 mov dword [esi + ebx + GEN9_PS_WIN_POS], 0 mov dword [esi + ebx + GEN9_PS_WIN_SZ], 0 DEBUGF 1, "vmode: Gen9 at the panel size, scaler off\n" jmp .arm ; the largest rectangle of the target's aspect inside the panel: compare ; target_w * panel_h with target_h * panel_w to find the edge that binds .scale: mov eax, [target_w] mul [info.panel_h] mov ecx, eax mov eax, [target_h] mul [info.panel_w] cmp ecx, eax jbe .height_binds mov eax, [info.panel_w] mov [window_w], eax mov eax, [target_h] mul [info.panel_w] div [target_w] mov [window_h], eax jmp .fitted .height_binds: mov eax, [info.panel_h] mov [window_h], eax mov eax, [target_w] mul [info.panel_h] div [target_h] mov [window_w], eax .fitted: ; even sizes and margins: the scaler takes no odd window and [window_w], not 1 and [window_h], not 1 mov eax, [info.panel_w] sub eax, [window_w] shr eax, 1 and eax, not 1 shl eax, 16 mov ecx, [info.panel_h] sub ecx, [window_h] shr ecx, 1 and ecx, not 1 or eax, ecx mov edx, [window_w] shl edx, 16 or edx, [window_h] mov dword [esi + ebx + GEN9_PS_CTRL], GEN9_PS_ENABLE mov dword [esi + ebx + GEN9_PS_PWR_GATE], 0 mov [esi + ebx + GEN9_PS_WIN_POS], eax mov [esi + ebx + GEN9_PS_WIN_SZ], edx DEBUGF 1, "vmode: Gen9 scaler window %x at %x\n", edx, eax ; the pipe, plane and scaler registers latch on a write to the surface ; address: write back the one that is there .arm: mov eax, [esi + edi + GEN9_PLANE_SURF] mov [esi + edi + GEN9_PLANE_SURF], eax ret ; ==================================================================== ; AMD DCE4: the running CRTC and the panel into info; CF = 1: none dce4_detect: push ebx esi edi cmp [bytes_pp], DCE4_BYTES_PP jne .none ; an integrated display controller is function 0 of a device on bus 0 xor ebx, ebx .device: invoke PciRead32, 0, ebx, PCI_VENDOR cmp ax, DCE4_VENDOR jne .next_device invoke PciRead32, 0, ebx, PCI_CLASS shr eax, 16 cmp ax, PCI_CLASS_VGA je .found .next_device: add ebx, 1 shl 3 cmp ebx, 32 shl 3 jb .device jmp .none .found: invoke PciRead32, 0, ebx, DCE4_BAR_LOW mov edi, eax and edi, not 0xF test eax, PCI_BAR_64BIT jz .have_bar invoke PciRead32, 0, ebx, DCE4_BAR_HIGH test eax, eax jnz .above_4g .have_bar: invoke MapIoMem, edi, DCE4_MMIO_SIZE, MMIO_FLAGS test eax, eax jz .no_map mov esi, eax xor ebx, ebx xor edi, edi .crtc: mov eax, [esi + edi + DCE4_CRTC_CONTROL] cmp eax, -1 je .next_crtc test eax, DCE4_CRTC_MASTER_EN jnz .got_crtc .next_crtc: add edi, DCE4_CRTC_STEP inc ebx cmp ebx, DCE4_CRTCS jb .crtc DEBUGF 1, "vmode: DCE4, no CRTC is running\n" jmp .unmap ; a pitch in pixels and a widened surface mean nothing on a tiled one .got_crtc: mov eax, [esi + edi + DCE4_GRPH_CONTROL] mov ecx, eax and ecx, DCE4_GRPH_DEPTH_MASK cmp ecx, DCE4_GRPH_DEPTH_32BPP jne .not_linear32 shr eax, DCE4_GRPH_ARRAY_SHIFT and eax, DCE4_GRPH_ARRAY_MASK cmp eax, DCE4_GRPH_ARRAY_LINEAR ja .not_linear32 ; the active area is the blank start less the blank end mov eax, [esi + edi + DCE4_CRTC_H_BLANK] call dce4_active jc .no_timing mov [info.panel_w], eax mov eax, [esi + edi + DCE4_CRTC_V_BLANK] call dce4_active jc .no_timing mov [info.panel_h], eax mov eax, [info.panel_w] mov [info.max_w], eax mov [info.fixed_h], 1 mov [info.family], FAMILY_DCE4 mov [mmio], esi mov [unit], edi clc jmp .return .not_linear32: DEBUGF 1, "vmode: DCE4 surface is not linear 32 bit, GRPH_CONTROL %x\n", \ [esi + edi + DCE4_GRPH_CONTROL] jmp .unmap .no_timing: DEBUGF 1, "vmode: DCE4 CRTC reports no active area\n" .unmap: invoke FreeKernelSpace, esi jmp .none .above_4g: DEBUGF 1, "vmode: DCE4 registers are above 4G\n" jmp .none .no_map: DEBUGF 1, "vmode: cannot map the DCE4 registers\n" .none: stc .return: pop edi esi ebx ret ; eax = a blanking register: eax = the active size; CF = 1: none, the pair ; does not read as a blank start and a blank end dce4_active: mov ecx, eax and ecx, DCE4_BLANK_MASK ; the blank start shr eax, 16 and eax, DCE4_BLANK_MASK ; the blank end sub ecx, eax jbe .none mov eax, ecx clc ret .none: stc ret ; ==================================================================== ; esi = registers, edi = the CRTC; the surface widens to the target and the ; horizontal ratio follows. CF = 1: refused, nothing written. dce4_set_size: ; the pitch register counts pixels of a 256 byte aligned line, and the whole ; surface has to stay inside the framebuffer the kernel maps mov eax, [target_w] add eax, DCE4_PITCH_ALIGN - 1 and eax, not (DCE4_PITCH_ALIGN - 1) mov [new_pitch_px], eax imul eax, DCE4_BYTES_PP mov [new_pitch], eax mul [target_h] test edx, edx jnz .no_room cmp eax, LFB_LIMIT ja .no_room ; source over destination, 26 fractional bits of which the hardware keeps 13 mov eax, [target_w] mov ecx, DCE4_SCL_RATIO_ONE mul ecx div [info.panel_w] and eax, not DCE4_SCL_RATIO_DROPPED mov ebx, eax mov dword [esi + edi + DCE4_CRTC_UPDATE_LOCK], 1 or dword [esi + edi + DCE4_GRPH_UPDATE], DCE4_GRPH_UPDATE_LOCK mov dword [esi + edi + DCE4_GRPH_X_START], 0 mov dword [esi + edi + DCE4_GRPH_Y_START], 0 mov eax, [target_w] mov [esi + edi + DCE4_GRPH_X_END], eax mov eax, [target_h] mov [esi + edi + DCE4_GRPH_Y_END], eax mov [esi + edi + DCE4_DESKTOP_HEIGHT], eax mov eax, [new_pitch_px] mov [esi + edi + DCE4_GRPH_PITCH], eax mov eax, [target_w] shl eax, 16 or eax, [target_h] mov dword [esi + edi + DCE4_VIEWPORT_START], 0 mov [esi + edi + DCE4_VIEWPORT_SIZE], eax mov [esi + edi + DCE4_SCL_SRC_SIZE], eax mov [esi + edi + DCE4_SCL_H_RATIO], ebx ; the surface registers latch on a write to the surface address: write back ; the one that is there mov eax, [esi + edi + DCE4_GRPH_SURF_ADDR] mov [esi + edi + DCE4_GRPH_SURF_ADDR], eax and dword [esi + edi + DCE4_GRPH_UPDATE], not DCE4_GRPH_UPDATE_LOCK mov dword [esi + edi + DCE4_CRTC_UPDATE_LOCK], 0 DEBUGF 1, "vmode: DCE4 pitch %u px, ratio %x\n", [new_pitch_px], ebx mov eax, [new_pitch] mov [lfb_pitch], eax clc ret .no_room: DEBUGF 1, "vmode: DCE4 %u bytes a line do not fit the framebuffer\n", \ [new_pitch] stc ret ; ==================================================================== ; the service, the kernel's display and the registers of both families API_VERSION = 1 ; the requests of the service, the same in programs/system/vmode/vmode.asm SRV_GETVERSION = 0 VMODE_GET_INFO = 1 ; out: VMODE_INFO VMODE_SET_SIZE = 2 ; in: the width, the height FAMILY_GEN9 = 1 FAMILY_DCE4 = 2 struct VMODE_INFO family dd ? panel_w dd ? panel_h dd ? firmware_w dd ? ; the size the kernel had when VMODE loaded firmware_h dd ? max_w dd ? ; the widest size SET_SIZE takes fixed_h dd ? ; nonzero: the height must be panel_h ends ; display_t, kernel/trunk/const.inc DISPLAY_WIDTH = 0x08 DISPLAY_HEIGHT = 0x0C DISPLAY_BPP = 0x10 DISPLAY_PITCH = 0x1C PCI_VENDOR = 0x00 PCI_CLASS = 0x08 ; the class in the high word PCI_CLASS_VGA = 0x0300 PCI_BAR_64BIT = 4 ; type 10b in bits 2:1 MMIO_FLAGS = PG_SW + PG_NOCACHE ; Intel Gen9, offsets from the Linux i915 headers: intel_display_regs.h and ; skl_universal_plane_regs.h. The display is always PCI 0:2:0, its registers ; are BAR0 (GTTMMADR) and the display blocks all live below 1M. GEN9_DEVFN = 2 shl 3 GEN9_BAR_LOW = 0x10 GEN9_BAR_HIGH = 0x14 GEN9_MMIO_SIZE = 0x100000 GEN9_PIPES = 3 GEN9_PIPE_STEP = 0x1000 GEN9_PIPESRC = 0x6001C ; (w - 1) shl 16 + h - 1 GEN9_PLANE_CTL = 0x70180 GEN9_PLANE_ENABLE = 0x80000000 GEN9_PLANE_STRIDE = 0x70188 ; bits 9:0, in 64 bytes GEN9_PLANE_STRIDE_MASK = 0x3FF GEN9_PLANE_STRIDE_UNIT = 64 GEN9_PLANE_POS = 0x7018C GEN9_PLANE_SIZE = 0x70190 ; (h - 1) shl 16 + w - 1 GEN9_PLANE_SURF = 0x7019C ; a write arms the pipe, plane and scaler GEN9_PS_PIPE_STEP = 0x800 ; the scalers of the next pipe GEN9_PS_STEP = 0x100 ; the second scaler of a pipe GEN9_PS_PWR_GATE = 0x68160 GEN9_PS_WIN_POS = 0x68170 ; x shl 16 + y GEN9_PS_WIN_SZ = 0x68174 ; w shl 16 + h GEN9_PS_CTRL = 0x68180 ; enabled; binding 0 is the pipe rather than a plane, mode 0 is dynamic and ; filter 0 is medium GEN9_PS_ENABLE = 0x80000000 ; AMD DCE4, offsets from drivers/video/drm/radeon/evergreen_reg.h except the ; scaler's two, which the tree does not declare - DCE4 drives its scaler ; through the ATOM EnableScaler table: ; 0x6D80 the horizontal ratio, source over destination, 1.0 = 0x04000000: ; 26 fractional bits of which the hardware keeps the top 13. A ; Wrestler running a 1024 wide surface on a 1366 wide panel reads ; 0x02FFA000, which is round(1024 * 2^13 / 1366) shl 13. ; 0x6D88 a second copy of the scaler's source size, in the shape of ; VIEWPORT_SIZE; read as 0x04000300 next to a 1024x768 viewport. ; The register aperture is BAR 0x18, 64K or 256K large (BKDG 43170 rev 3.13, ; D1F0x18); everything here lives below 64K. DCE4_VENDOR = 0x1002 DCE4_BAR_LOW = 0x18 DCE4_BAR_HIGH = 0x1C DCE4_MMIO_SIZE = 0x10000 DCE4_CRTCS = 2 DCE4_CRTC_STEP = 0xC00 DCE4_GRPH_CONTROL = 0x6804 ; bits 1:0 depth, 22:20 array mode DCE4_GRPH_DEPTH_MASK = 3 DCE4_GRPH_DEPTH_32BPP = 2 DCE4_GRPH_ARRAY_SHIFT = 20 DCE4_GRPH_ARRAY_MASK = 7 DCE4_GRPH_ARRAY_LINEAR = 1 ; anything above is tiled DCE4_GRPH_SURF_ADDR = 0x6810 ; a write arms the surface registers DCE4_GRPH_PITCH = 0x6818 ; in pixels DCE4_GRPH_X_START = 0x682C DCE4_GRPH_Y_START = 0x6830 DCE4_GRPH_X_END = 0x6834 DCE4_GRPH_Y_END = 0x6838 DCE4_GRPH_UPDATE = 0x6844 DCE4_GRPH_UPDATE_LOCK = 1 shl 16 DCE4_DESKTOP_HEIGHT = 0x6B04 DCE4_VIEWPORT_START = 0x6D70 DCE4_VIEWPORT_SIZE = 0x6D74 ; w shl 16 + h DCE4_SCL_H_RATIO = 0x6D80 DCE4_SCL_SRC_SIZE = 0x6D88 DCE4_SCL_RATIO_ONE = 0x04000000 DCE4_SCL_RATIO_DROPPED = 0x1FFF ; the low bits the hardware drops DCE4_CRTC_H_BLANK = 0x6E04 ; blank end shl 16 + blank start DCE4_CRTC_V_BLANK = 0x6E34 DCE4_BLANK_MASK = 0x1FFF DCE4_CRTC_CONTROL = 0x6E70 DCE4_CRTC_MASTER_EN = 1 DCE4_CRTC_UPDATE_LOCK = 0x6ED4 DCE4_BYTES_PP = 4 DCE4_PITCH_ALIGN = 256 / DCE4_BYTES_PP ; pixels: a line 256 byte aligned ; the kernel maps four large pages of framebuffer at LFB_BASE, see ; kernel/trunk/video/framebuffer.inc LFB_LIMIT = 0x1000000 ; ==================================================================== service_name db 'VMODE', 0 ; 16 characters at most with the zero align 4 info VMODE_INFO mmio dd ? ; the mapped registers unit dd ? ; the offset of the pipe or the CRTC scaler dd ? ; Gen9: the offset of the pipe scaler lfb_pitch dd ? bytes_pp dd ? target_w dd ? target_h dd ? window_w dd ? ; Gen9: the scaler window window_h dd ? new_pitch dd ? ; DCE4: the pitch of the target, bytes new_pitch_px dd ? ; and in pixels include_debug_strings ; without a relocation table every absolute address stays at the link time ; base and the kernel loads us somewhere else entirely align 4 data fixups end data