Skip to content

display-output: the compositor presents to the display driver, and tgl-display answers at vblank (stage 3) - #1134

Merged
Japabu merged 6 commits into
mainfrom
display-stage3
Nov 1, 2026
Merged

Japabu merged 6 commits into
mainfrom
display-stage3

Conversation

@Japabu

@Japabu Japabu commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Draft, stacked on #1132 (display-stage2, 166ea4dca).

Problem

PSR (stage 4) needs to know which frame changed, and where. Without that, PSR1 sees a CPU write to the firmware's surface only through FBC host tracking of a graphics-aperture fence. Vol 12 "CPU Host Front Buffer Tracking" limits that to "projects that support that". The compositor knows both, so it says them.

What this PR does

Stage 3 of issues/each-gpu-drives-its-displays-from-a-userland-driver.md, in the shape the orchestrator approved on 2026-11-01.

  • lib/display-output (toyos-display-output) owns the protocol. Every frame is decoded once.
    • Hello{size} is the driver's first frame: the output it scans out.
    • Present{sequence, damage} names one to eight rectangles, each inside that output.
    • Presented{sequence, vblank_nanos, refresh_interval_nanos} is the driver's answer.
    • Presenting keeps one present outstanding. Damage composed meanwhile is merged, past eight rectangles into their bounding box. That bounding box is the one region a PSR2 selective update takes anyway. An answer to no outstanding present is the driver's error.
  • The compositor serves display-output to one driver at a time. After each blit it hands the composed regions to Presenting, and it never waits on the driver. A refusal, a frame out of protocol, a hello for another size than the desktop's or a hello later than the handshake timeout drops the driver by name.
  • tgl-display offers the firmware's plane (PLANE_SIZE now gives its height) and answers each present at the vblank after it arrived:
    • interrupt::claim masks and disables every display interrupt category but pipe A's.
    • arm clears pipe A's identity and enables vblank only while a present waits, so an idle desktop takes no 60 Hz interrupt.
    • serve follows Vol 12's "Interrupt Service Routine" and disarms on the vblank. A pending category it enabled nothing in is refused by name. The PCH (SDE) category's registers are not in Vol 2c, so they are not zeroed.
    • The wait for a vblank has a hang ceiling of 84 ms.
    • When a connection a present arrived on ends from the compositor's side (a restart, a swap, or the compositor dropping the driver), the driver connects again, at most 8 times a boot. The compositor sends a present only once it has taken the hello. The queued connection and the next present are the events it waits on. A protocol refusal, or a connection that ended before any present arrived on it, ends presents by name, and the driver disarms its interrupts on the way out.
  • Every compositor row serves the port, and tgl-display receives it. metalcase now runs tgl-display beside hda.

Decisions

  • Vblank by interrupt, not by polling the frame counter. A 50 µs poll held for a frame would cost the package C-states this track exists to reach.
  • vblank_nanos is the claim's interrupt record time, a little after the vblank's start.
  • The compositor does not pace frames by Presented yet. It still blits the firmware's framebuffer itself until stage 5 moves that into a driver. The answer's fields are the final protocol's.

Checks

Review r2, at e884ee2f6: a connection counts as live once a present arrives on it; host CI exit 0, 107 steps green. The docs and the track say what the code does.

Review r1, at 4d54cce89:

  • cargo run -- --ci host exit 0, 107 steps green.
  • The r3 T14 readback re-judged offline at this head: display_output_presents PASS.
  • The compositor no longer panics on a late hello: toyos_display_output::Output owns the handshake, with host tests for a hello past the deadline, frames out of protocol and frames with handles.
  • The interrupt comments are corrected.
  • The judge rule has two fixtures.
  • Evidence: https://github.com/ToyOSOrg/evidence/blob/bb6b65cc3ff47e2f25cc08a1721a0ef4966fb59e/r4/README.md

r2, at f256164ab: host CI exit 0, 107 steps green; --build-only exit 0. The run before had a red: suite_split wanted the new job in RUST_SKIP, which it now is. Evidence: https://github.com/ToyOSOrg/evidence/blob/dc1d7cb98586f2fc299f1f36b9c5a7aa09db503e/r2/README.md

r1, at 4fb5d22f0 (the rebase changed none of this code):

  • cargo run -- --ci host: exit 0, 107 steps green.

  • cargo test -p toyos-display-output: 6 pass. They cover:

    • every frame round-trips;
    • rectangles outside the output, empty, overflowing, partial, or more than eight are refused;
    • a malformed hello or answer is refused;
    • merging past eight rectangles;
    • one present outstanding with the damage meanwhile sent next;
    • unasked answers.
  • cargo test -p toyos-tgl-display: 34 pass, 3 of them new interrupt tests:

    • nothing is raised until armed;
    • the vblank before an arm is not the one raised, and an armed vblank is raised once;
    • an unknown category is refused and display interrupts stay off.

    The T14 test now pins 1920x1080 and a refresh interval of 16.67 ms.

  • Negative controls, each a checked mutation restored in the same run, exit 101 each:

    • Presenting sending while a present is outstanding;
    • arm not clearing identity first.
  • cargo run -- --build-only: exit 0.

  • the_compositor_drives_the_virtio_gpu (QEMU): exit 0, PASS. The compositor starts with the new port and no driver, and its presents go nowhere. It ran before the last edits, which touched only tgl-display's main, metalcase, a metal row and docs, none of which this test reaches.

Evidence: https://github.com/ToyOSOrg/evidence/blob/e3e4100f568640262500f27f3abd1aee4e5755bf/r1/README.md

T14 boot (f256164): ran, exit 0; judged PASS

Evidence: https://github.com/ToyOSOrg/evidence/blob/33149bbebcb2b4ceedfe7736dd8a8757ba907120/r3/README.md

  • display_output_presents PASS, judged offline on the readback after the tone judge's fix. At its own head it was red, because the judge held hda's offer to before the tone job's spawn, and this job spawns at boot and waits for DC6. f566873 and its follow-up judge at the job's stream.
  • DC6 was allowed on DDI 0x1, with PWR_WELL_CTL 0x05/0x0f, DC_STATE_EN 2 and AUDIO_PIN_BUF_CTL bit 31 clear.
  • Pipe A's frame went from 421 at the allow to 453 at the first answered present.
  • tone_after_dc6 played whole after the DC6 line, with no underrun.
  • hda walked its codecs and offered its output before PG3 went down. So the analog codec is shown playing on after PG3 went down, not hda walking its link with PG3 already off.

Risks

  • QEMU cannot run tgl-display. The protocol's end to end exists only on the T14 until stage 5 makes virtio-gpu a driver.
  • If the GOP left a PCH display interrupt enabled, the first such event refuses the interrupt path by name, and presents stop.
  • metalcase on the T14 now runs the display driver, so its other rows boot with tuned watermarks and DC6 allowed.

🤖 Generated with Claude Code

@Japabu

Japabu commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1 of #1134 at ebba90d13

I took the protocol's shape and the virtio-gpu gap as settled. Register facts are checked against Vol 2c-12.21 (DISPLAY_INT_CTL, DE_PIPE_INTERRUPT, AUD_INTERRUPT, GFX_MSTR_INTR) and Vol 12 "Display Interrupts". The T14 evidence is r3 33149bbeb, checked against its raw kernel.log.

Architecture

The ownership is right:

  • lib/display-output owns the protocol between two programs, with its own types and no toyos-abi.
  • The compositor serves the protocol, and the driver connects as a client.
  • The kernel is unchanged: vblank arrives through the claim's existing interrupt record.
  • Presenting keeps the one-outstanding rule and the damage merge in a pure type the compositor cannot get wrong.

Production grows by about 620 lines: the protocol about 300, the compositor about 170, tgl-display's interrupt and main about 175. Tests grow by about 330: about 110 for the protocol, about 50 for interrupts, the model, the judge and tone_after_dc6.

What I verified

  • Decoding at each boundary.
    • Hello refuses a zero size and any length but 8.
    • Presented refuses a zero interval and any length but 24.
    • Present refuses a frame under 8 bytes, a partial rectangle, 0 or more than 8 rectangles, an empty rectangle, and any edge past the output, with checked adds.
    • Presenting::presented refuses any sequence but the one outstanding.
    • Neither reader can index out of range: every u32_at/u64_at follows an exact length check.
  • The compositor does not block on the driver.
    • It reads with FrameRx, which does not block, up to 4 frames a turn.
    • Each present is one try_send, and with one outstanding a correct driver never fills the channel.
    • A driver that never answers only grows the owed damage, which is bounded at 8 rectangles.
  • The damage merge. The ninth rectangle folds everything into a single bounding box, so union cannot overflow on rectangles clipped to the output. The tests pin both the merge and what is sent next.
  • The interrupt path, against Vol 12's ISR steps 1–8.
    • Vol 2c says a category is pending only while IIR & IER. Zeroing the unused IERs therefore suffices, even though their IIRs are never cleared.
    • arm clears IIR before it sets IER. The IIR latches whenever IMR is unmasked, so a vblank that started before the present can never be the one answered.
    • A vblank that latches between serve's clear and IER = 0 leaves IIR set with IER off. That raises no message and no storm, and the next arm clears it.
    • The IER addresses are right: 4441Ch/2Ch/3Ch, 4444Ch, 4446Ch, 4447Ch and 4448Ch.
    • The PCH (SDE) registers really are absent from Vol 2c.
  • Sequences. A second present while one is outstanding ends the connection, and a present is answered only by a vblank after its arm.
  • T14 evidence.
    • The hello was taken (1920x1080), and the first present was answered at frame 453, after the allow's 421.
    • The interval was 16 672 745 ns, which matches 2080×1112 / 138 727 kHz.
    • The tone played whole after DC6.

Required

  1. system/compositor/src/session.rs:511, :533, :581: a late hello crashes the compositor.
    • The bug. output_ready is computed at :511, before the overdue sweep at :533 drops the driver. Then :581 does self.output.as_mut().expect("a ready driver is held").
    • The failure. A driver connects and its first frame (or its EOF) arrives after HANDSHAKE_TIMEOUT (2 s). That wakes the poll, so the driver is both readable and overdue in the same turn. It is dropped and then unwrapped, and the compositor panics. The desktop goes with it.
    • When it happens. An idle desktop makes it likely: nothing else wakes the poll first, so the late frame is what wakes it.
    • Why it blocks. This is the "never stall on a slow or hostile driver" claim turned into a crash.
    • The fix. Compute output_ready after the sweep, or replace both expects with if let Some(driver) = self.output.as_mut().
  2. system/tgl-display/src/main.rs:120-123: no reconnect, and the gap is not recorded.
    • The PR's claim is false. The PR says this is "a recorded gap". No issues/ file mentions it. The track's stage 3 text (issues/each-gpu-…md:87-92) says nothing about a connection's end.
    • The trigger is a supported operation. The compositor is a service, and swap replaces a running service's binary: a connect made in the gap waits in the port's queue (system/supervisor/src/main.rs:26-30). So a compositor swap, which is ordinary development, ends presents for the boot.
    • What it costs. In this stage that costs only diagnostics. In tgl-display: PSR1 on the T14's panel, enabled from the compositor's first present (stage 4) #1136 it turns PSR off for the boot. In stage 5, where the driver scans out the compositor's surfaces, it means no display.
    • The comment's reason is wrong. "A connection the compositor refused would only be refused again" is true only of a protocol refusal. It is false for the most likely end, the compositor going away.
    • To resolve, either:
      • reconnect on EOF when the ended connection had its hello taken (it answered at least one present). That cannot loop on a hello the compositor refuses, and a refusal later than that happens at most once per present. Or:
      • record the gap in the track with its exit (before stage 5 at the latest) and correct the comment and the PR body.
    • Reconnecting looks cheaper than the bookkeeping.
  3. False or overclaiming comments in the interrupt code.
    • system/tgl-display/driver/src/regs.rs:125 says "Each IMR is 4 below its IER." It is 8 below: Vol 2c lists ISR +0, IMR +4, IIR +8, IER +Ch. The code (ier - 8, interrupt.rs:15) is right, and the comment would lead a reader to "fix" it.
    • system/tgl-display/driver/src/interrupt.rs:9 says "Every display interrupt but pipe A's vblank masked and disabled". The PCH (23) and PICA (25) categories are not touched. Say "every category Vol 2c gives registers for", as regs.rs already does.
  4. No evidence for the head commit.
    • ebba90d13 exists to fix a red host test (metal_audio_judges, which went red after f5668732e). The PR's Checks stop at f256164ab, and the T14 verdict was judged with f5668732e's marker (opening stream), not head's (client N connected).
    • I read the raw readback: spawn … tone_after_dc6 11.546, playing to the output 11.556, client 0 connected (id=0) 12.144. So head's judge should pass, but that is my inference, not a recorded run.
    • Cheapest sufficient check: cargo run -- --ci host at ebba90d13, plus an offline re-judge of the r3 readback at head, both recorded in the PR.

Optional

  • Handles. output.rs:57 and tgl-display/src/main.rs:180 ignore RxStep::Frame { handles, .. }. The protocol carries none, so a frame with handles could be refused as a protocol error rather than its batch silently dropped.
  • Tests for answer. main.rs:141-197 holds real decisions: a second present outstanding, the 84 ms give-up, and the present/irq order. That contradicts the header's "Every decision is toyos-tgl-display's", and leaves the loop without a host test. Moving that state into the driver library would make it testable against the model, which already has vblank().
  • Disarm on stop. When answer returns, pipe A's IER can stay armed and the display/primary enables stay set. That costs at most one stray message. Disabling them on the way out leaves a driver that has stopped serving with nothing armed.
  • The drop message. output.rs:68 drops a driver with {unasked:?} (Debug output). A Display line naming the sequence and the one outstanding would read like the other drop reasons.
  • The changed judge rule has no host fixture. The rule on_an_output judges by changed twice in this PR, once found on the bench and once by CI. Two fixture cases in tests/checks/audio.rs would hold it:
    • a job spawned before hda's offer whose stream connects after it passes;
    • a job whose stream connects before the offer fails.
  • The Refusal doc. driver/src/lib.rs:63 names only keep_memory_low_power and allow_display_c6, but interrupt::serve now returns Refusal::Interrupts too.

No reconnect: verdict

As it stands, it is a defect, not an acceptable decision. The compositor's restart or swap is a supported path, and the driver is the one process that cannot be restarted. The stated reason does not cover the common case. Required item 2 above gives the two ways to resolve it.

SEND BACK

Japabu and others added 5 commits November 1, 2026 16:23
…l-display answers at vblank (stage 3)

PSR needs to know which frame changed, and where. Without that, PSR1 sees
a CPU write to the firmware's surface only through FBC host tracking of a
graphics-aperture fence, which Vol 12 limits to "projects that support
that". The compositor knows both, so it says them.

`lib/display-output` (`toyos-display-output`) owns the protocol, in the
orchestrator's approved shape:
- the driver's first frame is `Hello{size}`, the output it scans out;
- the compositor sends `Present{sequence, damage}`, one to eight
  rectangles each inside that output;
- the driver answers `Presented{sequence, vblank_nanos,
  refresh_interval_nanos}`.
One present is outstanding at a time (`Presenting`). Damage composed
meanwhile is merged, past eight rectangles into their bounding box, which is
the one region a PSR2 selective update takes anyway. Every frame is decoded
once, and an answer to no outstanding present is the driver's error.

The compositor serves `display-output` to one driver at a time. After each
blit it hands the composed regions to `Presenting`, and it never waits on
the driver. A refusal, a frame out of protocol, a hello for another size
than the desktop's or a hello later than the handshake timeout drops the
driver by name.

tgl-display offers the firmware's plane (PLANE_SIZE now gives its height)
and answers each present at the vblank after it arrived:
- `interrupt::claim` masks and disables every display interrupt category
  but pipe A's, and enables display and primary interrupts.
- `arm` clears pipe A's identity and enables its vblank only while a
  present waits.
- `serve` follows Vol 12's service routine and disarms on the vblank. A
  pending category this driver enabled nothing in is refused by name.
- The wait has a hang ceiling of FRAME_CEILING_NANOS.
- One connection is served. When it ends, why is said and presents stop,
  since a driver the compositor dropped would be dropped again.

Every compositor row serves the port, and tgl-display receives it.
metalcase runs tgl-display beside hda, so its new T14 row
`display_output_presents` shows a present answered, and hda playing with PG3
down, in a boot the other metalcase rows already take.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ugh it

Review r1 of #1132 asks for one T14 boot showing `hda` and the panel
unharmed by stage 2. This row's metalcase boot already runs tgl-display
beside `hda`; what it lacked was order and the panel.

- `tone_after_dc6` waits in this boot's log for tgl-display's DC6 allow,
  then plays `audio_tone`'s muted tone through `hda` to its end, so the
  analog codec is shown playing with PG3 down. A refusal of DC6 fails it
  by name. metalcase's test-runner now receives `volume`, which the
  muted tone needs.
- tgl-display logs pipe A's frame counter at its first answered present.
  The judge holds it past the counter logged with the allow: the timing
  generator ran on after DC6 was allowed.
- The judge requires DC6 allowed on DDI A with DC_STATE_EN 2. It says
  whether `hda` walked its codecs before or after PG3 went down.
- `audio::tone_played` is `tone_on_metal` for any job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not its spawn

`on_an_output` held soundserver's first output offer to before the tone
job's spawn. `tone_after_dc6` spawns at boot and waits for tgl-display's
DC6 allow before it plays, so on the T14 (f256164) it was judged to play
to the null sink, though its stream opened half a second after `hda`'s
output was offered. The claim is about the stream, so the judge reads the
first `soundserver: opening stream` after the spawn. For a job that plays
at once nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f one carries

f566873 marked a tone job's stream by "soundserver: opening stream".
The harness's own audio fixture has soundserver say only "client N
connected", as every real log also does, so `metal_audio_judges` went red.
The judge now takes the first client connection soundserver says after
the spawn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the interrupt comments true

- A driver whose first frame came after the handshake timeout woke the
  compositor readable and overdue in one turn. Readiness was computed
  before the sweep dropped it, and the read unwrapped what had gone, so
  the compositor panicked. The connection's state is now
  `toyos_display_output::Output`, which refuses a hello past its deadline
  in the very call that reads it. The session computes readiness after the
  sweep and unwraps nothing. Host tests cover:
  - a hello one nanosecond late;
  - every way out of protocol, a frame carrying handles among them;
  - a served output's answers.
- The driver connects again when a compositor that was answering presents
  goes away, a restart or a `swap`, at most 8 times a boot, and says each
  attempt. The queued connection, and the compositor's next present, are
  the events it waits on; there is no delay. A protocol refusal, or a
  compositor gone before it took a hello, ends presents by name. Frames
  carrying handles are refused, since the protocol has none. On the way
  out the driver disarms everything (`interrupt::release`).
- `regs.rs`: each IMR is 8 below its IER, not 4. `interrupt::claim` names
  what it masks: every category Vol 2c has registers for, with PCH and PICA
  left as found and refused by `serve` when pending.
- Two harness fixtures hold `on_an_output` to the stream's connection: an
  output offered between a waiting job's spawn and its stream passes, and
  one offered after the stream fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2 of #1134 at 4d54cce89 (r1's items only)

I checked a2b3e9733..4d54cce89 (+278/−71 lines in 9 files) against r1's four required items. The new production code is Output (about 90 lines in lib/display-output), the reconnect loop and release (about 45 lines in tgl-display), and the compositor's adaptation, which came out smaller. The new tests are 3 for Output, 1 for release and 2 judge fixtures. I took the protocol's shape, the ruling's bound of 8 and the declined optional items as settled.

Architecture

Moving the handshake into toyos_display_output::Output is the right fix, not a workaround:

  • The state ("hello awaited until a deadline, then served") now lives in one pure type that the compositor only drives.
  • The deadline is checked in the same call that reads the frame, so no readiness flag can go stale.
  • The compositor's Driver is left with IO.

r1 items

  1. No compositor panic on driver timing: resolved.
    • session.rs sweeps first and computes output_ready after, and the read is behind if let Some. No expect on self.output is left.
    • A frame read late in a turn whose sweep came before the deadline still ends Late: Output::received checks overdue(now) first.
    • I traced Output for panics:
      • received has no unwrap, and its match covers every (frame, state) pair.
      • composed returns None before a hello.
      • u32_at/u64_at follow exact length checks.
      • union sees only rectangles clipped to the output.
      • The one expect left (output.rs:88) holds by intersect with the output's own rectangle.
    • Tests cover the hello 1 ns late, the hello taken and never overdue, every out-of-state pair, and handles.
  2. Reconnect per the ruling: resolved in code. One doc sentence overclaims; see Required.
    • A reconnect needs Gone { answered: true }, so each attempt costs at least one present answered at a vblank. It cannot spin.
    • reconnects is local to the one serve call per boot, so the bound is at most 8 a boot. Past it the loop stops with a line naming the count.
    • The loop has no delay. The next connection is made at once and waits in the port's queue (supervisor contract, system/supervisor/src/main.rs:26-31).
    • A service that ends with no restart owed has its acceptors closed, so endow::service returns ServerGone and the loop breaks by name. It does not retry.
    • Refused (a protocol error, an interrupt refusal or the 84 ms ceiling) and Gone { answered: false } break by name.
    • Every exit after claim reaches interrupt::release, and a_released_claim_raises_nothing_even_armed holds it.
    • Arming left over from a connection that ended with a present outstanding costs at most one stray vblank. serve disarms it, and with outstanding empty nothing is sent.
    • Waits. The only unbounded wait is poller.wait(…, u64::MAX) while no present is outstanding, which is a server waiting for its next request. With a present outstanding the wait is bounded by FRAME_CEILING_NANOS and ends Refused. The saturating_sub can return 0, and that is followed by the overdue return, not by another turn of the loop.
  3. The two comments: resolved.
    • regs.rs:125-126 now says 8, with the ISR/IMR/IIR/IER offsets.
    • interrupt.rs:9-13 names what claim masks and what it leaves.
    • The Refusal doc now names interrupt::serve.
  4. Host CI and the offline re-judge: resolved.
    • Evidence bb6b65cc r4: ci-host.log line 1 is 4d54cce89, it ends with Host(None): 107 step(s), all green / exit 0, and it includes the three new Output tests, a_released_claim… and metal_audio_judges.
    • judge.txt is stamped 4d54cce89: display_output_presents PASS, frames 421 → 453, exit 0.

Required

  • system/tgl-display/src/main.rs:139-140 and issues/each-gpu-drives-its-displays-from-a-userland-driver.md:92: "a compositor gone before it took a hello" ends presents. The code ends them in a wider case.
    • What the code tests. It ends presents for a compositor gone before this driver answered a present (Ended::Gone { answered: false }).
    • The failure. The compositor takes the hello. The desktop is idle, or its first present is still waiting for its vblank. The owner then swaps the compositor. The driver reads EOF with answered == false, says "presents go unanswered — the compositor hung up", and stops for the boot. The doc and the track both say it would connect again.
    • Why it matters. This is the contract tgl-display: PSR1 on the T14's panel, enabled from the compositor's first present (stage 4) #1136 and stage 5 will rely on, so it should say what the code tests. The code itself follows the ruling ("a connection that was answering presents").
    • The fix. Say "or a compositor gone before a present was answered" in both places. Ended::Gone's doc at :127-128 is already accurate. The PR body repeats the sentence; correcting it is the orchestrator's job.

Optional

  • main.rs:174-175: set answered when a present arrives, not when it is answered.
    • The compositor sends a present only once Output is serving, so a present received already proves the hello was taken.
    • That narrows the gap above to a compositor gone between taking the hello and its first present. The doc would then read "before it presented".
  • main.rs:127: "The compositor went away" is not the only meaning of EOF.
    • EOF also follows the compositor dropping the driver for the driver's own error (Unasked, a present try_send refused). The bound of 8 contains that.
    • A note that EOF may mean either would keep the name honest.
  • The changed code has not run on metal.

LAND once the Required sentence is corrected. It is a comment and issue-record fix, so it needs no new run. Until then:

SEND BACK

The compositor sends a present only once it has taken the hello, so a
present arriving proves the connection was served. The driver now
connects again after any such connection ends from the compositor's side,
so a compositor swapped while the desktop was idle, or before its first
present reached a vblank, is reconnected to. Before, the flag was set
only when a present was answered. The bound of 8 still contains a loop.
`Ended::Gone`'s doc says what EOF can mean: the compositor going away, or
it dropping the driver for a fault of the driver's own. `serve`'s doc and
the track say exactly what the code does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from display-stage2 to main November 1, 2026 15:50
@Japabu
Japabu marked this pull request as ready for review November 1, 2026 15:58
@Japabu
Japabu added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 190f3b1 Nov 1, 2026
10 checks passed
@Japabu
Japabu deleted the display-stage3 branch November 1, 2026 16:17
Japabu added a commit that referenced this pull request Nov 1, 2026
…irst present (stage 4)

The engine enters DC5 with the pipe lit only while PSR is active (Vol 12
"Display C5 and C6"), so stage 2's DC6 saves little without it.

The source side follows Vol 2c and Vol 12 throughout. The panel's side
comes from the clean-room brief of VESA facts and is recorded in the PR's
evidence:
- the AUX framing;
- DPCD 00070h-00071h, 00170h, 00600h and 02006h-02009h;
- the setup-time table.

- `aux`: native reads and writes on DDI A's channel, by Vol 12 "AUX
  Functional Sequence".
  - The data registers are MSbyte first.
  - The timeout is the 4 ms maximum.
  - The fast-wake count is 7 and the sync count 31, Vol 2c's values.
  - DEFER, NACK and short replies are retried, with the reply time-out
    between attempts.
  - A transaction runs only with PSR off and DC states disallowed, by the
    sequence's step 2.
- `psr::prepare`, before DC6:
  - refuses PSR already on, a PSR AUX error, a delayed vblank and a
    disabled watermark level. The last two need a latency-reporting change
    this driver does not make;
  - powers AUX A's IO;
  - reads the panel's PSR version and capabilities, eDP capability, error
    status and status, and refuses a panel without PSR, without SET_POWER,
    with a reserved setup time or with errors;
  - holds the setup time to vertical blank less a line (Vol 2c SRD_CTL);
  - tells the panel the main link stays active, then enables it and sets
    D0;
  - enables the VSC DIP with hardware's header and data;
  - masks max sleep, memory-up and hotplug, and unmasks the exit and
    AUX-error identities.
- `Prepared::activate` enables SRD. Context restore is kept, TP1 and TP2
  take 2.5 ms, TP4 is skipped, and idle frames are at least 6.
- `Psr::update` flips the surface to itself, a frame update event. `awake`
  reads SRD_STATUS's state, `disable` awaits IDLE within Vol 12's bound,
  and `fault` disables on a PSR AUX error.

Vol 2c's SRD_CTL says transcoder A's SRD "only operate[s] in standby".
The brief's vendor-code fact says Tiger Lake turns the link off. The PRM
decides, so the panel is told the link stays active, and the boot reads
both SRD_STATUS's state (010b link off, 111b link on) and its link field.

tgl-display enables PSR only at the compositor's first present, and
disables it when the connection ends. Nothing else would end SRD for a
change, and the compositor's first present is now the whole output as it
stands when it reads the hello. A present is answered at the first vblank
out of SRD. The driver says each of the first 16 presents' latency and SRD_STATUS,
then windows of 16 split by whether SRD had to end first, with
SRD_PERF_CNT. The T14 row `psr_exit_latency` rides
display_output_presents' boot.

The panel is read apart from `psr::prepare` (`psr::read_sink`), with DPCD
00000h-0000Fh, the receiver capabilities: its DPCD revision is never 0
from a receiver that answered, and a zero is refused by name
(`NoReceiver`). Every byte read is said before anything decides on it.
The T14's first boot (8230661) refused PSR on a zero at 00070h and said
nothing else of the panel.

Rebased onto #1134's review fixes:
- The whole first present is `toyos_display_output::Output`'s: a taken
  hello answers it (`Received::Offered { first }`), and the compositor
  sends it.
- A connection that ends from the compositor's side is connected to
  again. `Psr` keeps its `Prepared`, so `disable` hands it back with the
  panel still configured, and the next connection's first present enables
  SRD again with no AUX. A PSR AUX error disables PSR for the boot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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