Skip to content

perf: cut upload allocations and read real fan RPM - #1

Merged
JustinMDotNet merged 2 commits into
mainfrom
perf/upload-alloc-and-fan-rpm
Jul 23, 2026
Merged

perf: cut upload allocations and read real fan RPM#1
JustinMDotNet merged 2 commits into
mainfrom
perf/upload-alloc-and-fan-rpm

Conversation

@JustinMDotNet

@JustinMDotNet JustinMDotNet commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Performance/correctness changes from the full-repo performance audit. Byte-exact protocol behavior is preserved; 21/21 tests pass (build clean, 0 warnings).

Changes

  • NVAPI transport — drop the per-transfer clone. NvApiI2cBus.Write/Read now pin the caller's buffer with fixed instead of ToArray() + GCHandle, removing an allocation on every command, upload chunk, and RGB packet. Enables AllowUnsafeBlocks in AorusLcd.Core. The pinned pointer is used only inside the synchronous NVAPI call.
  • Static upload buffering — chunk directly. ProtocolFrames.BuildUpload gains a (prefix, payload) overload that chunks the logical descriptor+frame concatenation directly. Image/text uploads no longer allocate a ~108 KB LOH concat buffer, and the intermediate List<byte[]> is gone. The single-payload overload now delegates to it. Single-use ByteOps removed. A new byte-parity test locks the prefix path to the old concat-then-chunk output (chunk count, padding, and F1 header fields).
  • Fan dashboard unit — read real RPM. NVML fan speed now uses nvmlDeviceGetFanSpeedRPM (the unit the panel E3 field expects) instead of feeding a 0–100% value into an RPM field, with a graceful fallback to the percentage API on drivers that lack the export.

No fan control is added. This is read-only telemetry; the fan stays on the GPU's own (auto) curve.

Testing

  • dotnet build -c Debug — clean, 0 warnings.
  • dotnet test — 21/21 pass, including the new BuildUpload_PrefixMatchesConcatenatedPayload parity test.

Notes

Hardware-mandated pacing and byte-exact protocol/RLE behavior are untouched; these are allocation/units changes only. Robustness items from the audit (config watch-before-read, cancellable bus-lock) and the conditional GIF-memory rewrite are intentionally left for follow-ups.

Reduce avoidable allocations on the hardware path and fix the fan
dashboard unit, from the performance audit. Byte-exact protocol
behavior is preserved (21/21 tests pass, incl. a new parity test).

- NvApiI2cBus.Write/Read pin the caller buffer with `fixed` instead of
  ToArray()+GCHandle, removing a clone on every command/chunk/RGB packet
  (enables AllowUnsafeBlocks in AorusLcd.Core).
- ProtocolFrames.BuildUpload gains a (prefix, payload) overload that
  chunks the descriptor+frame directly; image/text uploads no longer
  allocate a ~108 KB LOH concat buffer, and the intermediate chunk list
  is gone. ByteOps (single-use) removed.
- NVML fan speed now reads real RPM via nvmlDeviceGetFanSpeedRPM, the
  unit the panel E3 field expects, with a graceful fallback to the
  percentage API on older drivers. Read-only telemetry only; the fan
  stays on the GPU's own (auto) curve.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fb67349-2267-4fa6-8d59-f8fc946e89ef

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR applies performance-focused changes across the upload path and hardware telemetry: it removes avoidable allocations during NVAPI I2C transfers and static upload frame construction, and it corrects the fan-speed unit used for the panel dashboard by reading RPM where available via NVML.

Changes:

  • Add a ProtocolFrames.BuildUpload(prefix, payload, ...) overload to chunk logical concatenations without allocating a combined buffer, with parity test coverage.
  • Remove per-transfer buffer cloning in NvApiI2cBus by pinning spans directly (enabling unsafe blocks in AorusLcd.Core).
  • Update NVML fan telemetry to prefer real RPM (nvmlDeviceGetFanSpeedRPM) with fallback to the percent API.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/AorusLcd.Tests/ProtocolTests.cs Adds a parity test ensuring the new prefix+payload upload builder matches concatenated payload output.
src/AorusLcd.Gui/Services/HardwareService.cs Switches static frame upload to the new non-concatenating BuildUpload(prefix, payload, ...) overload.
src/AorusLcd.Core/Sensors/NvmlSensorSource.cs Reads fan speed as RPM when available; falls back to percent on older NVML exports.
src/AorusLcd.Core/Sensors/Nvml.cs Adds P/Invoke surface for nvmlDeviceGetFanSpeedRPM and its FanSpeedInfo struct/version constant.
src/AorusLcd.Core/ProtocolFrames.cs Implements prefix+payload chunking to avoid large concatenation allocations.
src/AorusLcd.Core/Nvapi/NvApiI2cBus.cs Pins caller-provided buffers directly for NVAPI I2C calls to avoid per-call allocations.
src/AorusLcd.Core/ByteOps.cs Removes the now-unneeded concat helper.
src/AorusLcd.Core/AorusLcd.Core.csproj Enables unsafe blocks needed for the updated NVAPI I2C implementation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/AorusLcd.Core/ProtocolFrames.cs Outdated
Compute the prefix+payload total length in a checked context so the
theoretically-possible (though unreachable for real panel payloads) int
overflow throws instead of silently wrapping to a negative value and
producing a corrupt nchunks/header.

Addresses PR review feedback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6b7edc47-a7cf-4ed7-ab28-19dbecf7bcd7
Copilot AI review requested due to automatic review settings July 23, 2026 05:44
@JustinMDotNet

Copy link
Copy Markdown
Collaborator Author

Good catch in principle, though I'll push back on part of it. Both operands are ReadOnlySpan<byte> lengths for LCD panel payloads (a fixed image/text descriptor prefix + framebuffer, ~108 KB in practice), so the combined logical payload would need to exceed 2 GB for int to overflow — not reachable on this hardware.

That said, the guard is free insurance, so I've wrapped the sum in a checked context (b0fc531): the impossible-but-catastrophic case now throws cleanly instead of silently wrapping negative and corrupting nchunks/the F1 header.

I did not restructure nchunks to int: once the checked add rules out a negative/overflowed total, everything derived from it is sound, and keeping nchunks as uint avoids churn and extra casts on the header-write path. Build clean (0 warnings), 21/21 tests pass.

@JustinMDotNet
JustinMDotNet merged commit ead55a6 into main Jul 23, 2026
2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/AorusLcd.Core/ProtocolFrames.cs:95

  • nchunks is declared as uint, but the subsequent for (int c = 0; c < nchunks; c++) compares int to uint, which won’t compile (CS0019). Using an int chunk count here also avoids the unchecked (int)nchunks cast for list capacity; cast to uint only when writing the header field.
        uint nchunks = (uint)(total / FrameSize + 1);
        var frames = new List<UploadFrame>((int)nchunks + 3)

@JustinMDotNet
JustinMDotNet deleted the perf/upload-alloc-and-fan-rpm branch July 23, 2026 05:48
JustinMDotNet added a commit that referenced this pull request Aug 11, 2026
## What
Make it impossible to construct the GPU I2C bus at the wrong speed. This
is the structural fix for the audit's top finding (#1), the root cause
behind the recurring panel-freeze patches (#17/#20/#21/#22).

> Stacked on `fix/bus-lock-hardening` (PR #27) -> `audit/integration`.
Base retargets as those merge.

## Problem
The GPU has one physical I2C engine shared by the LCD (`0x61`) and RGB
(`0x71`/`0x75`) controllers. It only behaves at 400 kHz and wedges
silently at the NVAPI default speed. `NvApiI2cBus`'s `speed` parameter
defaulted to `NvApiI2cSpeed.Default`, so safety depended on every call
site remembering to pass `Khz400`. Each past freeze was a call site that
forgot.

## Changes
- **Remove the unsafe default (compile-time guard).** `NvApiI2cBus`'s
`speed` (and `address`/`port`) parameters are now required, so no caller
can silently get the wedging default. The `NvApiI2cSpeed.Default` enum
member is kept (it documents the NVAPI value); only its use as a
parameter default is gone. Added a read-only `Speed` property for
testability; `BuildInfo` and the wire encoding are unchanged.
- **Single bus factory.** New `NvApiBusFactory` is the one production
place that builds an Aorus bus: `Panel(gpu)` -> `0x61` / port 1 / 400
kHz, `Rgb(gpu, addr)` -> addr / port 1 / 400 kHz. Port and speed are
enforced in one spot.
- **Route all construction through it.** `NvApiPanelLocator` and
`RgbLocator` build via the factory; the locators'
probe/detection/ordering/dispose logic is byte-for-byte unchanged. A
caller-supplied non-default `port` to `Locate` is still honored (direct
construction, still 400 kHz), not ignored.
- **Policy tests.** `NvApiBusFactoryTests` assert the invariant
(address/port/400 kHz) with no hardware, since the `NvApiI2cBus`
constructor is pure.

## Scope note
This is the "Core session" step the audit recommended as sufficient for
alpha; a fuller single-owner bus service (the other side of the "needs
human judgment" design fork) remains a possible future step. This PR
closes the accidental-default-speed root cause without an over-broad
rewrite.

## Testing
- `dotnet build -c Release` clean; `dotnet test -c Release` green
(52/52, +3 factory tests). AOT-safe.
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.

2 participants