Skip to content

custom_video: add a GUD backend for USB indirect displays - #106

Open
alphanu1 wants to merge 2 commits into
antonioginer:masterfrom
alphanu1:gud-backend
Open

custom_video: add a GUD backend for USB indirect displays#106
alphanu1 wants to merge 2 commits into
antonioginer:masterfrom
alphanu1:gud-backend

Conversation

@alphanu1

Copy link
Copy Markdown
Contributor

Adds modeline generation for a Generic USB Display — a GUD device, as carried by
the in-tree Linux drm/gud driver — driven on Windows by an IddCx indirect
display driver. The motivating case is a 15 kHz CRT on a board that accepts
arbitrary modelines over USB, where the point is a per-game timing rather than a
mode chosen from a fixed list. That already works on Linux through drmkms;
this is the Windows half.

Two commits: a one-line correctness fix in the factory, then the backend.

How it differs from the other Windows backends

It does not push timing into a graphics driver through a vendor interface,
because there is no graphics driver involved. The display's timings live in a
file the display driver watches, and it re-enumerates its monitor when that file
changes:

C:\ProgramData\gud-windows\modelines.ini    written by the backend
C:\ProgramData\gud-windows\modes.active     written by the driver

process_modelist() writes the modelines and waits for the driver to publish
them. Setting one is then an ordinary ChangeDisplaySettingsEx through
display_windows, because by that point Windows knows the mode. caps() is
ADD | UPDATE.

Why get_timing() matters as much as adding modes here

Windows reports every mode this display has as progressive. An IddCx target mode
set as interlaced comes back progressive and there is no way to express it, so
648x480i60 arrives as 480p at 60 Hz and its line rate computes as 31.5 kHz
rather than the 15.75 it actually runs. Uncorrected, that puts a 15 kHz
monitor's own modes out of range:

Switchres: [ 648]x[ 480]_[60=60.000000Hz]
   rng(0):  out of range
Switchres: normal (640x480@60.000000)->(632x240@60.000000)
   rng(0):  632 x 240_60.000000p ... scale(0.988, 0.500, 1.000)

A 480-line request lands on 240 lines with half of them discarded. The driver
knows the real timings and writes them to modes.active; get_timing() reads
them back and the modes classify correctly:

Switchres: [ 648]x[ 480]  rng(0):  648 x 480_60.000000i 15.750000 [integ]

The same asymmetry when setting a mode

set_desktop_mode() builds its DEVMODE with
dmDisplayFlags = mode->interlace? DM_INTERLACED : 0, and
ChangeDisplaySettingsEx returns DISP_CHANGE_BADMODE for an interlaced mode
against this display, since Windows has no interlaced mode to match it against.
The backend clears the flag once a mode becomes the OS's to set. Nothing real is
lost — interlace on a GUD display is a property of the link between the driver
and the board, not of anything Windows does. It stays in the modeline and the
device interleaves on read.

Does it affect anything else?

No, and it is gated deliberately tightly. The backend claims a display only when
the driver's published list names that devicemodes.active carries a
device line holding the hardware id, matched on VID/PID against
EnumDisplayDevices' DeviceID. Testing for a USB device id alone would claim
any other USB-attached display on the machine, a DisplayLink adapter among them.
Matching a hardcoded VID/PID instead would tie this to one board, where the
driver serves any GUD device.

Verified on hardware:

condition result
PCI adapter (Intel UHD 620) Video chipset is not compatible → existing path, untouched
modes.active absent not claimed → Found 0 custom → system mode, identical to before
modes.active names another device not claimed → identical fallback
modes.active names this device claimed → Found 3 custom → generated modes

CUSTOM_VIDEO_TIMING_GUD is 0x400, inside CUSTOM_VIDEO_TIMING_MASK and
clear of the editable flags in the low bits and the MODE_* flags in the high
ones. Linux is untouched: the include and the factory branch are inside
#if defined(_WIN32) and the new file is only in the Windows SRC list.

Two things you will reasonably object to

It depends on a driver that is not upstream, and you cannot test it without
the hardware.
Both true. It is also the shape every backend here already has:
pstrip locates PowerStrip — third-party, commercial, not upstream — with
FindWindowA("TPShidden", NULL) and logs PowerStrip found!; adl needs AMD's
ADL; drmkms needs libdrm and a card under /dev/dri. Each is detected at
runtime and inert when absent, and this is the same: it compiles unconditionally
on Windows, claims nothing when the file is not there, and costs a fopen per
display at init. The driver is MIT and public, so the dependency is at least
inspectable. If you would still rather it were opt-in, a build flag or an ini
option is a small change and I am happy to make it.

The path is hardcoded. C:\ProgramData\gud-windows\ is the driver's own
fixed location — it reads and writes there and the backend does not get to
choose it — which is the same arrangement as drmkms and its device nodes, or
pstrip and its window class. It could be surfaced as a switchres.ini option
if you want it configurable; I left it out rather than add an option with one
possible value.

The first commit

custom_video::make() does
sscanf(device_id, "PCI\\VEN_%x&DEV_%x", &vendor, &device) on two uninitialised
locals and compares vendor to 0x1002 on the next line. Not every display has
a PCI id — a USB one reports USB\VID_1D50&PID_614D&REV_0100 — so the sscanf
assigns nothing and the comparison reads whatever was on the stack. Separated
out because it stands on its own.

Testing

Built with MinGW g++ (-Wall -Wextra, no new warnings) from a clean clone.
Tested on a blitsCRT_Mister against a 15 kHz CRT. Generated and displayed:

  • 384x224@59.1857.842 384 400 437 500 224 236 239 265
  • 640x480i6013.038 640 666 727 831 480 483 489 523 interlace

Both reached the CRT with their own porches, neither is advertised by the
device. Linux is unbuilt and unchanged by this.

The Windows display driver is gud-windows;
docs/DESIGN.md there covers the interlace behaviour and the modes.active
format.

On Windows the factory identifies the graphics vendor with

    int vendor, device;
    sscanf(device_id, "PCI\VEN_%x&DEV_%x", &vendor, &device);

    if (vendor == 0x1002) // ATI/AMD

Not every display device has a PCI id. An indirect display reached over
USB reports something like

    USB\VID_1D50&PID_614D&REV_0100

so the sscanf matches nothing, assigns nothing, and both locals are read
uninitialised on the line after. The comparison then depends on whatever
was on the stack, and can pick the ATI path on a machine with no AMD
hardware in it.

Initialise them. No functional change on a PCI display, where the sscanf
fills both in.
Adds modeline generation for a Generic USB Display -- a GUD device, as
carried by the in-tree Linux drm/gud driver -- driven on Windows by an
IddCx indirect display driver. The motivating case is a 15 kHz CRT on a
board that takes arbitrary modelines over USB, where the point is a
per-game timing rather than a mode picked from a fixed list.

Unlike the other Windows backends this one does not push timing into a
graphics driver through a vendor interface. There is no graphics driver
involved. The display's timings live in a file the display driver
watches, and it re-enumerates its monitor when that file changes:

    C:\ProgramData\gud-windows\modelines.ini    written here
    C:\ProgramData\gud-windows\modes.active     written by the driver

so process_modelist() writes the modelines and waits for the driver to
publish them. Setting one is then an ordinary ChangeDisplaySettingsEx,
because by that point Windows knows the mode.

get_timing() carries as much weight here as adding modes does. Windows
reports every mode this display has as progressive -- an IddCx target
mode set as interlaced comes back progressive, and there is no way to
express it -- so 648x480i60 arrives as 480p at 60 Hz and its line rate
computes as 31.5 kHz rather than the 15.75 it actually runs. Uncorrected
that puts a 15 kHz monitor's own modes out of range: asking for 640x480
on arcade_15 skips both 480-line modes and lands on 240 lines with half
of them thrown away. The driver knows the real timings and writes them
down; get_timing() reads them back, and the modes classify correctly.

The same asymmetry runs the other way when setting a mode.
set_desktop_mode() builds its DEVMODE with

    lpDevMode.dmDisplayFlags = mode->interlace? DM_INTERLACED : 0;

and ChangeDisplaySettingsEx returns DISP_CHANGE_BADMODE for an
interlaced mode against this display, since Windows has no interlaced
mode to match. So the backend clears the flag once a mode becomes the
OS's to set. Nothing real is lost: interlace on a GUD display is a
property of the link between the driver and the board, not of anything
Windows does. It stays in the modeline, and the device interleaves on
read.

The backend claims a display only when the driver's published list names
that device -- modes.active carries a device line holding the hardware
id, and it is matched on VID/PID against EnumDisplayDevices' DeviceID.
Testing for a USB device id alone would claim any other USB-attached
display on the machine, a DisplayLink adapter or a second indirect
display among them, and write generated modelines into a driver with
nothing to do with them. Matching a fixed VID/PID instead would tie this
to one board, where the driver serves any GUD device. Every other
display reaches the existing paths unchanged, PCI ones included.

Entries the backend owns are prefixed sr_ and rewritten as a set;
anything else in the ini is somebody's hand-written modeline and is
copied through untouched, the driver treating that file as
authoritative.

Tested on a blitsCRT_Mister board against a 15 kHz CRT. Generated
384x224@59.185 (7.842 384 400 437 500 224 236 239 265) and 640x480i60
(13.038 640 666 727 831 480 483 489 523 interlace), both reaching the
CRT with their own porches. Checked that a PCI adapter is untouched,
that a missing modes.active falls back to system mode, and that a
modes.active naming a different device is not claimed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant