UE5 Heterogeneous Multi-GPU: Unterschied zwischen den Versionen
Elaina (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
Elaina (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
||
| (19 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt) | |||
| Zeile 1: | Zeile 1: | ||
{{Hinweis|Projektstand: | {{Hinweis|Projektstand: 17. September 2026. Aktueller Engine-Checkpoint: <code>1fc0d24</code> (<code>Add DLSS5 NR integration and packaged shader support</code>) auf Branch <code>mgpu-experimental</code>. Der vorherige Checkpoint <code>37c6d02</code> bleibt die Grundlage des Worker-, Workload- und GPU-Timing-Pfads. Neu sind cook-fähige ExperimentalMultiGPU-Global-Shader sowie die Integration von [[DLSS5ForUE5]] als realer Neural-Rendering-Workload. Die beschriebenen Ergebnisse sind ein experimenteller Proof of Concept für Unreal Engine 5.8 unter Windows und Direct3D 12.}} | ||
'''UE5 Heterogeneous Multi-GPU''' ist ein experimentelles Projekt zur gemeinsamen Nutzung unterschiedlicher Grafikkarten in [[Unreal Engine 5]]. Eine leistungsfähige GPU übernimmt das normale Rendering. Weitere NVIDIA-, AMD- oder Intel-GPUs sollen als unabhängige Worker klar abgegrenzte Aufgaben bearbeiten. | '''UE5 Heterogeneous Multi-GPU''' ist ein experimentelles Projekt zur gemeinsamen Nutzung unterschiedlicher Grafikkarten in [[Unreal Engine 5]]. Eine leistungsfähige GPU übernimmt das normale Rendering. Weitere NVIDIA-, AMD- oder Intel-GPUs sollen als unabhängige Worker klar abgegrenzte Aufgaben bearbeiten. | ||
Das Ziel ist ausdrücklich '''kein klassisches SLI oder CrossFire'''. Die Anwendung verteilt Arbeit selbst und tauscht nur benötigte Ergebnisse zwischen den GPUs aus. | Das Ziel ist ausdrücklich '''kein klassisches SLI oder CrossFire'''. Die Anwendung verteilt Arbeit selbst und tauscht nur benötigte Ergebnisse zwischen den GPUs aus. Der Proof of Concept besitzt inzwischen einen durchgängigen Worker-Rasterpfad aus echten UE-Weltdaten bis zur zweiten physischen GPU und zurück in eine persistente UE-Textur. Eine backend-neutrale Workload-Schicht erfasst Aufgaben, Lifecycle-Daten und echte Worker-GPU-Zeiten; automatische Scheduling-Entscheidungen trifft sie noch nicht. Seit Commit <code>1fc0d24</code> funktioniert der ExperimentalMultiGPU-Shaderpfad außerdem in Development-Packages. Parallel liefert DLSS5 Neural Rendering einen ersten realen, klar messbaren Kandidaten für eine spätere grobe Worker-Auslagerung; aktuell läuft dieser Workload noch vollständig auf der Primary-NVIDIA-GPU. | ||
{| class="wikitable" | {| class="wikitable" | ||
| Zeile 19: | Zeile 19: | ||
! Worker | ! Worker | ||
| Intel UHD Graphics | | Intel UHD Graphics | ||
|- | |||
! Zusätzliche Testhardware | |||
| Intel Arc A750 und Arc A380 als zusätzliche dGPU-Worker-Gegenchecks vorgesehen; später außerdem RX 6900 XT ↔ Intel | |||
|- | |||
! Git-Stand | |||
| Commit <code>1fc0d24</code> auf Branch <code>mgpu-experimental</code>; vorheriger Checkpoint <code>37c6d02</code>; Working Tree nach dem Commit ohne ausstehende versionierte Änderungen | |||
|- | |- | ||
! Windows/D3D12-Proof-of-Concept | ! Windows/D3D12-Proof-of-Concept | ||
| | | Durchgängiger sichtbarer Worker-Rasterpfad von der laufenden UE-Welt über getaggte Static und Skeletal Meshes sowie eine echte SceneCapture2D-Kamera bis zur persistenten UE-Textur; dynamisch bis 3840×2160 und 4K60 mit 300/300 Ready-Frames nachgewiesen | ||
|- | |||
! Mess- und Workload-Schicht | |||
| Generische Workload-Registry, Runtime Metrics V1 und echte D3D12-Timestamp-Messungen des Worker-Rasterabschnitts implementiert | |||
|- | |||
! Packaged Build | |||
| Alle neun ExperimentalMultiGPU-Global-Shader nach RenderCore verlagert, beim Cook registriert und im Development-Package erfolgreich gestartet | |||
|- | |||
! Neural Rendering | |||
| [[DLSS5ForUE5]] in Source-Engine, Editor und Packaged Game integriert; Project-Config, native Caller-Gate-Bridge, Depth-/Motion-Guides und robuster Support-Fallback validiert | |||
|- | |- | ||
! Gesamtvision | ! Gesamtvision | ||
| etwa 30–35 % (Scheduler, echte Workloads, N-GPU und Vulkan noch offen) | | etwa 30–35 % (Kostenmodell, Scheduler, weitere echte Workloads, N-GPU und Vulkan noch offen) | ||
|} | |} | ||
| Zeile 31: | Zeile 46: | ||
<pre> | <pre> | ||
Intel Worker GPU | Intel Worker GPU | ||
-> | -> MainTextureCS + PatternPhase | ||
-> | -> persistente Worker-Textur | ||
-> Shared Cross-Adapter Buffer | -> persistenter Shared Cross-Adapter Buffer | ||
-> Shared Fence | -> monotone Shared Fence / Queue Wait | ||
-> | -> persistente UE-eigene FTextureRHIRef | ||
-> stabile UExperimentalMultiGPUTexture | |||
RTX Primary GPU | RTX Primary GPU | ||
-> normales UE5-Rendering | -> normales UE5-Rendering | ||
-> | -> TPS-Material zeigt das Worker-Ergebnis sichtbar an | ||
</pre> | </pre> | ||
Pro Worker ist maximal ein Runtime-Job gleichzeitig in flight. Backpressure lehnt weitere Requests als <code>Busy</code> ab, bis der Slot nach der Primary-Completion wieder sicher verwendbar ist. | |||
Jeder Worker besitzt ein eigenes <code>ID3D12Device</code>, eigene Queues, Command Lists und Ressourcen. Ein späterer Scheduler soll Rechenleistung, Fähigkeiten, Transferkosten und gewünschte Aktualisierungsrate bewerten. Vorgesehene Aufgaben sind beispielsweise SceneCapture2D, Spiegel, CCTV, Minimap, Compute oder Niagara. | Jeder Worker besitzt ein eigenes <code>ID3D12Device</code>, eigene Queues, Command Lists und Ressourcen. Ein späterer Scheduler soll Rechenleistung, Fähigkeiten, Transferkosten und gewünschte Aktualisierungsrate bewerten. Vorgesehene Aufgaben sind beispielsweise SceneCapture2D, Spiegel, CCTV, Minimap, Compute oder Niagara. | ||
| Zeile 49: | Zeile 67: | ||
Ein echter, von Unreal kompilierter Global Compute Shader läuft auf der Intel UHD, während Unreal regulär über die RTX rendert. Der Test verdoppelt 64 Ganzzahlen auf dem Worker; '''64 von 64 Ergebnissen wurden korrekt verifiziert'''. Damit ist echte GPU-Arbeit auf einem zweiten, herstellerfremden Device nachgewiesen. | Ein echter, von Unreal kompilierter Global Compute Shader läuft auf der Intel UHD, während Unreal regulär über die RTX rendert. Der Test verdoppelt 64 Ganzzahlen auf dem Worker; '''64 von 64 Ergebnissen wurden korrekt verifiziert'''. Damit ist echte GPU-Arbeit auf einem zweiten, herstellerfremden Device nachgewiesen. | ||
Zusätzlich läuft <code>MainTextureCS</code> auf der Intel-GPU. Der synchrone Debugtest erzeugt eine 64 × 64 Pixel große RGBA8-Textur; '''alle 4096 shader-generierten Pixel wurden korrekt verifiziert'''. Der normale Runtime-Pfad verwendet inzwischen einen asynchronen 128×96-Test. Der Shader liest seine Dimensionen mit <code>GetDimensions()</code>, schützt überhängende Threads durch einen Bounds-Check und wird mit <code>Dispatch(16,12,1)</code> ausgeführt. <code>PatternPhase</code> erzeugt für jeden akzeptierten Submit deterministisch einen neuen sichtbaren Inhalt. Abgewiesene Busy-Requests erhöhen die Phase nicht und erzeugen daher keine Lücken. | |||
=== Cross-Adapter-Speicher und Synchronisation === | === Cross-Adapter-Speicher und Synchronisation === | ||
| Zeile 54: | Zeile 74: | ||
* RTX und Intel öffnen denselben D3D12 Cross-Adapter Heap. | * RTX und Intel öffnen denselben D3D12 Cross-Adapter Heap. | ||
* Ein gemeinsamer Buffer wurde mit 64 Testwerten erfolgreich geprüft. | * Ein gemeinsamer Buffer wurde mit 64 Testwerten erfolgreich geprüft. | ||
* Ein Shared Fence synchronisiert die Queues direkt von GPU zu GPU. | * Ein persistenter Shared Fence synchronisiert die Queues direkt von GPU zu GPU. Seine Werte steigen pro Job monoton: WorkerReady/PrimaryComplete verwenden die Paare 1/2, 3/4, 5/6 und so weiter. | ||
* Die CPU | * Die CPU vermittelt die GPU-Abhängigkeit nicht; Worker- und Primary-Queue verwenden <code>Signal</code> und <code>Wait</code> direkt auf der GPU. | ||
* Der separate Readback-Verifikationstest darf weiterhin blockieren. Der normale Runtime-Pfad wartet dagegen nicht auf GPU-Leerlauf. | |||
=== Texturtransport === | === Texturtransport === | ||
| Zeile 62: | Zeile 83: | ||
<pre> | <pre> | ||
Intel-Textur | Intel MainTextureCS | ||
-> persistente Worker-Textur | |||
-> CopyTextureRegion | -> CopyTextureRegion | ||
-> Shared Cross-Adapter Buffer | -> persistenter Shared Cross-Adapter Buffer | ||
-> Shared Fence / RTX | -> Worker signalisiert WorkerReady (1, 3, 5, ...) | ||
-> | -> UE-Primary-Queue wartet auf WorkerReady | ||
-> | -> direkte Kopie in persistente UE-eigene FTextureRHIRef | ||
-> Primary signalisiert PrimaryComplete (2, 4, 6, ...) | |||
</pre> | |||
Der robuste Normal-Pfad kopiert die Worker-Textur über den Shared Buffer direkt in eine von Unreal erzeugte <code>FTextureRHIRef</code>. Breite, Höhe und Row Pitch werden dynamisch aus Worker-Ressource und Transport-Metadaten übernommen. Der erfolgreiche 128×96-RGBA8-Test verwendet 512 Byte Row Pitch und 49.152 Byte Transfergröße. Die Output Registry trennt Persistent-, Pending- und Ready-Zustände sowie Inhaltsgeneration und Texturidentität. Dadurch kann eine verspätete Completion keinen neueren Output zurückrollen. | |||
=== Non-blocking Runtime-Pfad === | |||
Die produktive Testlogik ist aus dem Konsolenkommando in eine gekapselte NormalPath-API verschoben. <code>SubmitWorkerTextureComputeJob()</code> reicht den Worker-Dispatch ohne CPU-Wait ein. <code>QueueNormalTextureTransfer()</code> reiht Wait, Kopie und Completion-Signal in Unreals D3D12-Kontext ein. Der normale MainTextureCS-Pfad enthält weder CPU-Fence-Waits noch Readback, <code>Map</code> oder Pixelvergleich. <code>TickNormalTextureTransfers()</code> prüft die Primary-Completion non-blocking über einen Runtime-Pump; <code>RHIEndFrame()</code> bleibt als Fallback erhalten. | |||
Eine passende Primary-<code>FTextureRHIRef</code> wird pro Worker und Konfiguration persistent gehalten. Nach PrimaryComplete kann der nächste Job dieselbe Resource erneut verwenden; dabei führt Unreal die Übergänge <code>SRVMask -> CopyDest -> SRVMask</code> aus. Eine backend-neutrale RHI-Provider-API reicht die Ready-Textur an die Engine weiter. <code>UExperimentalMultiGPUTexture</code> hält eine stabile <code>UTexture</code> samt TextureReference und bindet die persistente <code>FTextureRHIRef</code> ohne zusätzlichen Pixel-Copy. Eine native D3D12-Resource wird weiterhin nicht blind als UE-Textur gewrappt, weil Unreals Resource-State-Buchhaltung den tatsächlichen nativen Zustand sonst nicht sicher kennt. | |||
=== Sichtbarer TPS-Consumer === | |||
<code>AExperimentalMultiGPUDisplayActor</code> zeigt die Worker-Ausgabe im Third-Person-Testlevel über den Materialparameter <code>MultiGPUTexture</code> an. Damit ist der Output nicht mehr nur über Logs oder Readback nachgewiesen: Die Intel UHD erzeugt fortlaufend die Texturinhalte, während die RTX die Spielwelt und das Material rendert. <code>TextureConsumerCS</code> kann Ready-Ausgaben auf der Primary zusätzlich als SRV lesen und fünf quantisierte Samples im Debugpfad prüfen. | |||
=== Lebensdauer und automatisches Cleanup === | |||
Der Normal-Pfad verwendet pro Worker genau einen persistenten Runtime-Slot. Worker-Textur, Root Signature, PSO, Descriptor Heap, Command Allocator/List und Worker-Fence werden nach der ersten kompatiblen Initialisierung wiederverwendet. Dasselbe gilt für Shared Heap/Buffer/Fence und die Transport-Command-Objekte. Der In-Flight-Job trägt nur noch die nötigen Job-, Generation- und Fence-Metadaten. | |||
<pre> | |||
Runtime Request | |||
-> Completion non-blocking prüfen | |||
-> falls Slot frei: Worker Dispatch | |||
-> WorkerReady-Fence signalisieren | |||
-> Primary Wait, SafeCopy und PrimaryComplete-Signal | |||
-> Output Pending | |||
-> Runtime-Pump oder RHIEndFrame prüft GetCompletedValue() | |||
-> Output Ready publizieren | |||
-> Slot wiederverwendbar | |||
</pre> | |||
Backpressure erlaubt maximal einen echten In-Flight-Job pro Worker. Ein Busy-Request setzt keine GPU-Arbeit ab, setzt keine Ressourcen zurück und erhöht <code>PatternPhase</code> nicht. Worker-Fence-Werte steigen mit jedem akzeptierten Submit 1, 2, 3 und so weiter; der persistente Shared Fence verwendet fortlaufend die Paare 1/2, 3/4, 5/6. Fehler nach Übergabe von Ressourcen an die GPU führen zu sicherem Retire beziehungsweise Retention statt zu vorzeitigem Reuse. | |||
=== Texture Stream und Benchmarking === | |||
Der projektseitige Texture Stream akzeptiert eine beliebige positive, endliche Zielrate. Punkt und Komma sind als Dezimaltrenner möglich. Optional lässt sich die Zahl erfolgreicher Submits begrenzen; Busy- oder Failed-Versuche verbrauchen dieses Limit nicht. Der Quiet-Modus unterdrückt per-Job-Ausgaben, behält aber Start, echte Fehler und die Abschlussstatistik bei. | |||
Die Statistik unterscheidet Timer-Ticks, angenommene Submits, Busy/Failed, Ready, In-Flight und die jeweiligen effektiven Raten. Diagnosezeitpunkte erfassen unter anderem RequestAccepted, RenderCommandExecution, WorkerSubmit, NormalTransferQueued und ReadyObserved. | |||
{| class="wikitable" | |||
! Test | |||
! Ergebnis | |||
! Aussage | |||
|- | |||
| 5 Hz, 20 Submits | |||
| etwa 5 Ready/s, 20/20, kein Busy | |||
| Entspannter Single-Slot-Betrieb | |||
|- | |||
| 1000 Hz, 100 Submits, quiet | |||
| 100/100 Ready, 0 Fehler, etwa 58 Ready/s | |||
| Backpressure begrenzt den Durchsatz sicher | |||
|- | |||
| 1000 Hz bei <code>t.MaxFPS 30</code> | |||
| etwa 28–29 Ready/s trotz rund 982 Timer-Ticks/s | |||
| Primary-/SafeCopy-/Ready-Kette ist effektiv framegekoppelt | |||
|} | |||
Ein zusätzlicher non-blocking Aufruf von <code>ImmediateFlush(DispatchToRHIThread)</code> erreicht hohe Render-Command-Raten, hebt die gemessene Frame-Kopplung aber nicht auf. Im 30-FPS-Test lag der RHIEndFrame-spezifische Completion-Takt praktisch exakt bei 30 Hz. Das aktuelle Problem ist damit eingegrenzt: Nicht der Stream-Timer, sondern die Primary-RHI-/D3D12-Submission der SafeCopy-Kette bestimmt die sichtbare Ready-Rate. | |||
=== Queue-Submission-Diagnose === | |||
<code>MultiGPUD3D12QueueProbe</code> untersucht die Submission unabhängig vom sichtbaren Texturpfad. Neben <code>full</code>, <code>waitonly</code> und <code>signalonly</code> existieren inzwischen die Diagnosemodi <code>computeonly</code>, <code>commandonly</code>, <code>barrieronly</code>, <code>barrierprivate</code> und <code>copybuffer</code>. Als Queues stehen <code>graphics</code>, <code>copy</code>, <code>native</code>, <code>worker</code>, <code>worker-copy</code> und <code>worker-high</code> zur Verfügung. Graphics und Copy verwenden <code>RHIRunOnQueue(..., false)</code> auf UE-verwalteten Primary-Queues; die übrigen Varianten isolieren private beziehungsweise bereits vorhandene native D3D12-Queues. | |||
Der Probe misst Request, Worker-Submit, <code>RHIRunOnQueue</code>-Aufruf und -Callback, Primary-Execute, Fence-Completion sowie Ready bis zum nächsten akzeptierten Submit. Er fügt keine CPU-Waits, Readbacks, Maps oder <code>BlockUntilGPUIdle</code> ein und beschreibt keine Textur des sichtbaren Normal-Pfads. | |||
{| class="wikitable" | |||
! Test | |||
! Ergebnis | |||
! Befund | |||
|- | |||
| UE Graphics, signal-only, 30 FPS | |||
| 33,319 ms PrimaryExecute bis Ready | |||
| UE-verwaltete Graphics-Submission skaliert mit der Frame-Dauer | |||
|- | |||
| UE Copy, signal-only, 30 FPS | |||
| 32,593 ms PrimaryExecute bis Ready | |||
| Auch die UE-verwaltete Copy Queue ist framegekoppelt | |||
|- | |||
| Private Primary-DIRECT-Queue, signal-only | |||
| etwa 0,014 ms Queue/Fence-Latenz; nach Lifecycle-Fix rund 999 Ready/s bei 30 FPS | |||
| Native Queue und Scheduler sind vollständig vom Frame entkoppelt | |||
|- | |||
| Worker signal-only / leere Command List | |||
| jeweils rund 1000 Ready/s bei 30 FPS | |||
| Worker-Queue, Fence und reine Submission sind nicht framegekoppelt | |||
|- | |||
| Worker <code>CopyBufferRegion</code>, 30 / 60 / 120 FPS | |||
| 29,41 / 58,25 / 113,08 Ready/s | |||
| Bereits eine echte 256-Byte-GPU-Kopie folgt im UE-Prozess dem Frame-Raster | |||
|- | |||
| Worker COPY Queue / HIGH Priority, 30 FPS | |||
| jeweils rund 29,42 Ready/s | |||
| Queue-Typ und Priorität beseitigen die Kopplung nicht | |||
|- | |||
| Standalone-D3D12 auf derselben Intel UHD | |||
| 92.956 Copy-Jobs/s; 0,007 ms durchschnittliche Fence-Latenz | |||
| Die Kopplung ist UE-Prozess-/Scheduling-spezifisch, keine allgemeine Intel-, WDDM- oder D3D12-Grenze | |||
|} | |||
Der <code>RHIRunOnQueue</code>-Callback selbst erreicht den D3D12-Submission-Thread typischerweise bereits nach etwa 0,02–0,05 ms; die Verzögerung entsteht danach in der verwalteten Queue-/Submission-Reihenfolge. <code>SubmitCommandsHint()</code> ist in UE 5.8 nur ein veralteter Alias für <code>ImmediateFlush(DispatchToRHIThread)</code> und stellt daher keinen stärkeren Submission-Mechanismus dar. | |||
Der native Test ist ein '''Architekturbeweis, noch kein Experimental- oder Direct-Modus'''. Die zunächst gemessene Verzögerung von rund 32 ms lag zwischen der In-Flight-Freigabe und dem nächsten Request auf dem Render Thread. Für <code>signalonly/native</code> wurde der sichere Completion-/Submit-Lifecycle deshalb in den frameunabhängigen Scheduler verlegt. Danach erreichten zwei 100-Job-Läufe bei <code>t.MaxFPS 30</code> rund 999 bis 1029 Ready/s, ohne Busy oder Fehler. | |||
Der anschließende <code>full native</code>-Test lokalisierte die verbleibende Kopplung auf echte Worker-GPU-Arbeit: WorkerSubmit bis WorkerReady benötigte bei 30, 60 und 120 FPS ungefähr 33,29, 16,93 und 8,45 ms; WorkerReady bis PrimaryReady war danach praktisch sofort. Signal-only und eine leere Command List waren schnell, Resource Barriers und eine 256-Byte-Kopie dagegen framegebunden. Ein eigenständiger D3D12-Microbenchmark auf derselben Intel UHD erreichte außerhalb von Unreal bei derselben Kopie rund 92.956 Jobs/s mit 0,007 ms mittlerer Fence-Latenz. Die genaue Ursache im UE-Prozess beziehungsweise dessen GPU-Scheduling-Kontext bleibt offen und wird vorerst bis zum Gegencheck mit der vorhandenen Intel Arc A380 geparkt. | |||
=== Worker-Rasterpfad und RasterStream === | |||
<code>ExperimentalMultiGPU.TestRasterWorker</code> weist erstmals echte Rasterizer-Arbeit auf der Intel-Worker-GPU nach. <code>RasterWorkerVS</code> und <code>RasterWorkerPS</code> werden über Unreals Global-Shader-Pfad kompiliert und für ein natives Worker-Graphics-PSO verwendet. Ein persistentes 128×96-RGBA8-RenderTarget besitzt RTV, leere Root Signature und eine DIRECT-Command-List. Der Vertex Shader erzeugt drei Vertices über <code>SV_VertexID</code>; der Pixel Shader schreibt interpolierte RGB-Farben. | |||
Der Job wechselt das RenderTarget von <code>COPY_SOURCE</code> nach <code>RENDER_TARGET</code>, führt Clear und <code>DrawInstanced(3,1,0,0)</code> aus und wechselt anschließend zurück nach <code>COPY_SOURCE</code>. Danach verwendet er unverändert den robusten Normalpfad aus Cross-Adapter-Shared-Buffer, Primary SafeCopy, persistenter <code>FTextureRHIRef</code>, <code>UExperimentalMultiGPUTexture</code> und DisplayActor. Der Runtime-Test meldete einen akzeptierten Submit und <code>RasterWorker READY</code> für Generation 1; die 128×96-UTexture wurde erfolgreich gebunden. | |||
Der einmalige Test wurde außerdem zu einem frei konfigurierbaren RasterStream ausgebaut. Ein sichtbarer Lauf mit 10 Hz und 50 erfolgreichen Submits endete mit 50 Ready-Ausgaben, 0 Busy und 0 Fehlern bei etwa 9,90 Ready/s. Primary-Textur und Materialbindung blieben dabei persistent. | |||
=== Raster3D mit echten Vertex- und Indexbuffern === | |||
Der Worker besitzt inzwischen einen kleinen eigenständigen 3D-Rasterpfad mit perspektivischer Kamera, Model-View-Projection-Matrix als Root Constants, D32_FLOAT-Depth-Buffer und Depth-Test. Der finale Testwürfel verwendet 24 POSITION/COLOR-Vertices, 36 <code>uint16</code>-Indices und <code>DrawIndexedInstanced(36,1,0,0,0)</code>; sechs getrennte Seitenfarben machen Ausrichtung und Rotation sichtbar nachvollziehbar. | |||
Die statischen Meshdaten werden einmalig über private Upload-Buffer in DEFAULT-Heaps übertragen. Der Init-Fence wird non-blocking über <code>GetCompletedValue()</code> beobachtet. Der eigentliche Runtime-Renderpfad enthält weiterhin keine CPU-Waits und keine Maps. | |||
=== UE-Static-Meshes auf der Worker-GPU === | |||
<code>StaticMeshWorker</code> extrahiert echte LOD0-Geometrie aus <code>UStaticMesh</code>-Assets. POSITION und NORMAL sowie <code>uint32</code>-Indices werden in POD-Daten überführt, einmalig asynchron in eigene Worker-VB/IB hochgeladen und anschließend per <code>DrawIndexedInstanced</code> gerendert. Assetpfade sind dynamisch; Auto-Fit richtet Kamera beziehungsweise Modellmaßstab an den Bounds aus. | |||
Sichtbar korrekt getestet wurden: | |||
{| class="wikitable" | |||
! Asset | |||
! Vertices | |||
! Indices | |||
|- | |||
| Sphere | |||
| 559 | |||
| 2880 | |||
|- | |||
| Cube | |||
| 54 | |||
| 144 | |||
|- | |||
| Cylinder | |||
| 334 | |||
| 1536 | |||
|} | |||
=== Multi-Mesh Worker-Szene === | |||
<code>StaticMeshScene</code> rendert Sphere, Cube und Cylinder gemeinsam in einen Color-/Depth-Frame. Der verifizierte Test umfasst insgesamt 947 Vertices und 4560 Indices, drei <code>DrawIndexedInstanced</code>-Aufrufe und nur einen anschließenden Cross-Adapter-/Normal-SafeCopy-Transfer. Damit ist nicht mehr nur ein synthetisches Primitive, sondern mehrere echte UE-Meshes mit gemeinsamer Tiefenprüfung im Worker-Frame sichtbar nachgewiesen. | |||
=== WorldStaticMeshScene === | |||
<code>ExperimentalMultiGPUWorldStaticMeshScene.cpp</code> sammelt im Engine-Layer bis zu acht sichtbare <code>UStaticMeshComponent</code>s von Actors mit dem Tag <code>MultiGPUWorker</code>. Meshdaten und World-Transforms werden im Game Thread in reine POD-Strukturen und 4×4-Matrizen kopiert. D3D12RHI erhält keine <code>UObject</code>-Pointer. Mehrere Instanzen desselben Meshes teilen den persistenten Worker-Mesh-Cache und unterscheiden sich nur durch Modelmatrix und Draw Call. | |||
Der Pfad wurde inzwischen sichtbar im PIE-Level verifiziert. Drei getaggte Static-Mesh-Actors erschienen mit ihren tatsächlichen relativen Positionen, Rotationen und Skalierungen im Worker-Bild. Die zentrale Koordinatenumrechnung lautet: | |||
<pre>Worker (X, Y, Z) = (UE Y, UE X, -UE Z)</pre> | |||
Die Transformation besitzt die Determinante +1 und ist damit keine Spiegelung. Das notwendige horizontale Spiegeln erfolgt ausschließlich auf der Presentation-Plane, nicht im Worker-Renderpfad. | |||
=== Kontinuierlicher WorldStaticMeshSceneStream === | |||
<code>StartWorldStaticMeshSceneStream</code> erfasst die getaggte Szene fortlaufend im Game Thread. Reine Änderungen an Transform oder Kamera lösen keinen erneuten Mesh-Upload aus. Hinzugekommene, entfernte, unsichtbare oder umgetaggte Components werden im nächsten Snapshot berücksichtigt. Single-Slot-Backpressure verhindert einen wachsenden Rückstau. | |||
Ein verifizierter 30-Hz-Lauf endete nach 31,184 Sekunden mit 835 Ticks, 691 angenommenen Submits, 144 Busy, 0 Fehlern und 691 Ready-Frames. Die effektive Ready-Rate betrug 22,16 Hz. Dieser Test belegt die funktionale Live-Aktualisierung; die Rate war noch kein Optimierungsziel. | |||
=== SceneCapture2D als Worker-Kamera === | |||
<code>TestSceneCaptureStaticMeshScene</code> verwendet eine echte <code>ASceneCapture2D</code> beziehungsweise <code>USceneCaptureComponent2D</code> aus der laufenden PIE-Welt als Kameraquelle. Der empfohlene Editor-Weg ist das Component Tag <code>MultiGPUWorkerCamera</code> auf der <code>CaptureComponent2D</code>; ein gleichnamiges Actor-Tag bleibt nur als Legacy-Fallback. | |||
Ausgelesen werden World Location, Rotation, Forward/Right/Up, <code>FOVAngle</code> und <code>ProjectionType</code>. Orthografische Projektion wird derzeit sauber abgelehnt. Die SceneCapture2D liefert ausschließlich Kameradaten: Ihr UE-RenderTarget wird weder gelesen noch kopiert. Die Intel-Worker-GPU rendert die Geometrie selbst. Kamera und Actors werden gemeinsam mit <code>SceneScale = 0.01</code> in Worker-Koordinaten überführt; Auto-Fit und Scene-Recentering sind in diesem Modus deaktiviert. | |||
Die 128×96-Worker-Projektion verwendet ein Seitenverhältnis von 4:3 und interpretiert <code>FOVAngle</code> als horizontales FOV. Nach den Orientierungstests wird nur die Anzeige über <code>SetRelativeScale3D(FVector(-4.0f, -3.0f, 1.0f))</code> korrigiert; dies entspricht <code>U' = 1-U</code> und <code>V' = 1-V</code>. | |||
=== Live SceneCaptureStaticMeshSceneStream === | |||
<code>StartSceneCaptureStaticMeshSceneStream</code> liest Kamera-Transform, Kamerabasis, FOV und aktuelle Actor-Transforms pro Snapshot neu im Game Thread. Der Worker erhält weiterhin ausschließlich POD-Daten, verwendet den persistenten Mesh-Cache sowie ein gemeinsames Color- und D32-Depth-Target und führt pro akzeptiertem Frame genau einen Cross-Adapter-/Normal-SafeCopy-Transfer aus. | |||
Der Quiet-Test | |||
<pre>ExperimentalMultiGPU.StartSceneCaptureStaticMeshSceneStream 30 300 quiet</pre> | |||
lieferte in 10,084 Sekunden 301 Ticks, 300 Submits, 1 Busy, 0 Fehler und 300 Ready-Frames. Die effektive Ready-Rate betrug 29,75 Hz. Bewegungen der Kamera und Actors sowie FOV-Änderungen erschienen live auf der Display-Plane. Damit ist erstmals ein kleiner SceneCapture-/CCTV-artiger Worker-Kanal vollständig funktional nachgewiesen. | |||
=== Dynamische SceneCapture-Auflösung === | |||
Die SceneCapture-Ausgabe ist nicht mehr fest auf 128×96 Pixel begrenzt. <code>ExperimentalMultiGPU.SetSceneCaptureResolution <Width> <Height></code> setzt ganzzahlige Dimensionen von 1 bis 4096; <code>GetSceneCaptureResolution</code> zeigt die aktive Einstellung. 128×96 bleibt der Standard. Während ein SceneCapture-Stream läuft, wird ein Auflösungswechsel absichtlich abgelehnt, damit keine In-Flight-Ressourcen ausgetauscht werden. | |||
Color Target, D32-Depth-Buffer, Row Pitch, Shared-Buffer-Transport, SafeCopy, persistente Primary-Textur und <code>UExperimentalMultiGPUTexture</code> übernehmen die gewählten Dimensionen. Projektion und Display-Plane verwenden das echte Seitenverhältnis. Die bestätigte Anzeigeorientierung <code>U' = 1-U</code> und <code>V' = 1-V</code> bleibt eine reine Presentation-Korrektur; Worker-Basis und Kameramathematik ändern sich nicht. | |||
=== Scheduler-Korrektur bei SceneCapture === | |||
Ein 1920×1080-Test mit 25 Hz erreichte zunächst trotz fehlerfreier Frames nur 13,90 Ready-Hz und erzeugte 222 Busy-Ticks. Ursache war kein GPU-Limit, sondern ein Polling-Aliasing: Eine frühe Game-Thread-Prüfung konnte den Timer-Slot verwerfen, obwohl die Completion kurz darauf im CoreTicker/RHI-Pump sichtbar geworden wäre. | |||
Die frühe <code>IsRuntimeTextureUpdateInFlight()</code>-Prüfung wurde für diesen Stream entfernt. Der Pending-Request bleibt als Backpressure-Gate erhalten; die maßgebliche Busy-Entscheidung fällt nun erst im Render Command nach <code>TickNormalTextureTransfers(nullptr, true)</code>. Single-Slot, POD-Snapshots und non-blocking <code>GetCompletedValue()</code> bleiben erhalten. Es wurden weder CPU-Waits noch zusätzliche Threads eingeführt. | |||
Nach der Korrektur erreicht 1080p bei 25 Hz 24,79 Ready-Hz mit 1 Busy; bei 30 Hz sind es 29,43 Ready-Hz mit 4 Busy. | |||
=== Skalierung bis 4K60 === | |||
Die Pipeline wurde von 640×360 bis 3840×2160 getestet. Bei 640×360/30 Hz und 1280×720/30 Hz wurden 29,85 beziehungsweise 29,82 Ready-Hz ohne Busy erreicht. 1920×1080/60 Hz lieferte 59,40 Ready-Hz ohne Busy, 2560×1440/60 Hz 59,21 Ready-Hz mit 1 Busy und 3840×2160/60 Hz 59,02 Ready-Hz mit 2 Busy. | |||
Ein zusätzlicher 4K60-Stresstest mit acht Static-Mesh-Instanzen und drei gecachten Unique Meshes erreichte in 5,049 Sekunden '''300 von 300 Ready-Frames, 0 Busy, 0 Fehler und 59,42 effektive Ready-Hz'''. Acht Draw Calls, gemeinsamer Depth Buffer, Cross-Adapter-Transport und SafeCopy liefen dabei im vorhandenen Single-Slot. Das ist ein Stabilitätsnachweis der aktuellen Mini-Szene, kein allgemeiner GPU-Benchmark. | |||
=== Skeletal Mesh in Reference Pose === | |||
<code>ExperimentalMultiGPU.TestSkeletalMeshWorker</code> erweitert den Geometriepfad auf <code>USkeletalMeshComponent</code>. Der Engine-Layer sucht deterministisch die erste sichtbare, getaggte Komponente und extrahiert LOD0-Positionen, Normalen, Indices und Render Sections. D3D12RHI erhält wie bisher ausschließlich POD-Geometrie und Modelmatrizen. | |||
Der sichtbare Test mit <code>SKM_Quinn_Simple</code> rendert Quinn in der Reference Pose mit '''45.993 Vertices, 261.840 Indices, zwei Sections und zwei Draw Calls'''. Die beiden Sections umfassen 35.612 und 51.668 Dreiecke. Worker-VB/IB, gemeinsamer D32-Depth-Buffer, dynamische Auflösung, Cross-Adapter-Transport und Normal/SafeCopy werden wiederverwendet. | |||
Eine Reference Pose ist die unverformte Ausgangshaltung des Skeletal Meshes. Bone-Matrizen, Skin Weights, laufende Animationen, Morph Targets und Cloth sind noch nicht angebunden. Skeletal Skinning ist erst vorgesehen, wenn ein echter Worker-Workload wie CCTV oder Spiegel animierte Figuren in seiner eigenen Ansicht benötigt. | |||
=== Generische Workload-Registry === | |||
Commit <code>37c6d02</code> führt erstmals eine backend-neutrale Workload-Schicht ein. Sie beschreibt Aufgaben unabhängig vom konkreten Grafik-Backend und bildet die Grundlage für einen späteren Scheduler, trifft selbst aber noch keine GPU-Entscheidung. | |||
{| class="wikitable" | |||
! Bestandteil | |||
! Inhalt | |||
|- | |||
| Workload-Typ | |||
| <code>Unknown</code>, <code>SceneCapture</code>, <code>Compute</code> oder <code>Raster</code> | |||
|- | |||
| Gewünschte Zuweisung | |||
| <code>Primary</code>, <code>Worker</code> oder <code>Auto</code>; <code>Auto</code> ist derzeit nur Metadatum | |||
|- | |||
| Priorität | |||
| <code>Low</code>, <code>Normal</code> oder <code>High</code> | |||
|- | |||
| Descriptor | |||
| Sessionweit monotone ID, Typ, Zuweisung, Priorität, Auflösung, Zielrate, Debugname und Running-Status | |||
|} | |||
Die Registry stellt <code>RegisterWorkload</code>, <code>UpdateWorkload</code>, <code>UnregisterWorkload</code>, <code>GetWorkload</code> und <code>GetActiveWorkloads</code> bereit und schützt ihre Daten per <code>FCriticalSection</code>. Descriptoren enthalten weder <code>UObject</code>- noch D3D12-Pointer. Der SceneCapture-Stream registriert genau einen Worker-Workload mit normaler Priorität und entfernt ihn bei Stop oder Auto-Stop wieder vollständig. | |||
<code>ExperimentalMultiGPU.ListWorkloads</code> zeigt die aktiven Descriptoren und ihre Laufzeitwerte an. | |||
=== Runtime Metrics V1 === | |||
Descriptor und Laufzeitstatistik sind getrennt. Pro Workload werden Ticks, angenommene Submits, Busy- und Fehlerfälle, fertige Frames, aktuell ausstehende Jobs, Laufzeit sowie effektive Tick-, Submit- und Ready-Raten erfasst. Der SceneCapture-Pfad spiegelt dafür seine vorhandenen Stream-Zähler in die Registry, statt eine zweite Statistiklogik aufzubauen. Die Messwerte werden derzeit nur beobachtet und ändern weder Zuweisung noch Auflösung oder Zielrate. | |||
Editor-Fokus und Background-Throttling können die Lifecycle-Raten deutlich verfälschen. In einem sauberen 1280×720-Referenzlauf wurden 1000/1000 Ready-Frames ohne Busy bei 29,84 Ready-Hz gemessen; bei wechselndem Fensterfokus entstanden dagegen 459 Busy-Ticks und nur 16,69 Ready-Hz, ohne dass die Worker-GPU-Zeit entsprechend einbrach. Solche Raten sind daher kein verlässlicher Ersatz für echte GPU-Kostenmessungen. | |||
=== Worker GPU Timing V1 === | |||
Der SceneCapture-Worker verwendet persistente native D3D12-Timestamp-Queries auf seiner vorhandenen DIRECT-Queue: einen Query Heap mit zwei Slots, einen kleinen Readback-Buffer und die per <code>GetTimestampFrequency()</code> ermittelte Zeitbasis. Das Ergebnis wird erst nach non-blocking geprüftem Fence-Abschluss gelesen; CPU-Waits entstehen nicht. Die Workload-ID wird als POD bis zum Worker mitgeführt, während einmalige Tests die ID 0 verwenden. | |||
Gemessen wird ausschließlich der Worker-Rasterabschnitt: | |||
<pre> | |||
Start Timestamp | |||
-> COPY_SOURCE nach RENDER_TARGET | |||
-> Color- und Depth-Clear | |||
-> alle DrawIndexedInstanced-Aufrufe | |||
-> RENDER_TARGET nach COPY_SOURCE | |||
End Timestamp | |||
-> ResolveQueryData | |||
</pre> | </pre> | ||
Nicht enthalten sind der Cross-Adapter-Transfer, die Primary-SafeCopy und die Integration auf der Primary-GPU. Erfasst werden Sample-Anzahl sowie letzter, mittlerer, minimaler und maximaler Messwert. | |||
{| class="wikitable" | |||
! Auflösung | |||
! Samples | |||
! Mittelwert | |||
! Minimum | |||
! Maximum | |||
|- | |||
| 1280×720 | |||
| 845 | |||
| 0,166 ms | |||
| 0,165 ms | |||
| 0,181 ms | |||
|- | |||
| 3840×2160 | |||
| 599 | |||
| 0,683 ms | |||
| 0,594 ms | |||
| 0,751 ms | |||
|} | |||
Bei identischem, bewusst einfachem Szeneninhalt mit sieben Draw Calls steigt die gemessene Worker-Rasterzeit damit ungefähr um Faktor 4,1. Die Pixelzahl allein erklärt die Skalierung nicht vollständig. Für ein belastbares Kostenmodell müssen als Nächstes Transfer und Primary-Integration separat gemessen werden. | |||
=== Cook- und Packaged-Support der Multi-GPU-Shader === | |||
Vor Commit <code>1fc0d24</code> startete ein Packaged Build nicht, weil unter anderem die Permutation von <code>FExperimentalMultiGPUStaticMeshPS</code> in der cooked Global Shader Map fehlte. Die Shader selbst waren korrekt; ihre neun <code>FGlobalShader</code>-Typen wurden jedoch im D3D12RHI registriert, das beim Cook zu diesem Zeitpunkt noch nicht geladen war. | |||
Die Deklarationen liegen nun im RenderCore-Header <code>ExperimentalMultiGPUShaders.h</code>, die zugehörigen <code>IMPLEMENT_GLOBAL_SHADER</code>-Registrierungen in <code>ExperimentalMultiGPUShaders.cpp</code>. D3D12RHI verwendet nur noch die exportierten RenderCore-Typen. Shaderpfade, Entry Points und die bestehende Shader-Blob-Erkennung bleiben unverändert. Der Cook kompilierte die ExperimentalMultiGPU-Shader nachweislich; das anschließende Development-Package startete ohne den früheren <code>Missing global shader</code>-Fatal. Damit ist der Worker-Pfad shaderseitig nicht mehr auf den Editor beschränkt. | |||
=== DLSS5 Neural Rendering als späterer Worker-Kandidat === | |||
== | Der Branch enthält nun auch [[DLSS5ForUE5]] als quelltextbasierte Win64-/D3D12-Integration für NVIDIA Neural Rendering. Das Plugin verwendet den bereits vom offiziellen NVIDIA-DLSS-Plugin initialisierten NGX-Core und eröffnet keine konkurrierende NGX-Application-Session. Neural Rendering läuft derzeit auf der Primary-NVIDIA-GPU; ein Multi-GPU-Offload ist noch nicht implementiert. | ||
Die Integration umfasst: | |||
* eine eigene <code>Config/DLSS5ForUE5.ini</code> mit 20 projektbezogenen Einstellungen, | |||
* einen ausschließlich im Editor wirksamen Safe Boot, der Neural Rendering und Scene Guides nur für die aktuelle Session deaktiviert, | |||
* eine native <code>DLSS5ForUE5_nvngx.dll</code> als unmittelbaren Caller für den NVIDIA-Caller-Gate, | |||
* Depth- und Motion-Guides aus der laufenden UE-Szene, | |||
* die Supportzustände <code>AVAILABLE</code>, <code>UNAVAILABLE</code>, <code>UNSUPPORTED</code> und <code>WAITING</code> mit sauberem Fallback statt wiederholtem Init-Logspam. | |||
Im monolithischen Packaged Game wurde der NGX-Init zunächst mit <code>0xBAD00002</code> abgelehnt, weil der Aufruf aus der Spiel-EXE kam. Eine native Bridge löst dieses Problem. Zusätzlich musste die MSVC-Tail-Call-Optimierung verhindert werden: Erst ein echter <code>call</code> mit erhaltenem Bridge-Stackframe ließ die DLL als unmittelbaren Caller sichtbar bleiben. Danach meldete die Runtime Caller Gate, NGX-Core, Parameter und Initialisierung erfolgreich. In einem Packaged-Lauf waren 2036 von 2036 Evaluierungen erfolgreich; Depth und Motion waren jeweils angefordert, verfügbar und verwendet. | |||
Der Pfad wurde sowohl im kleinen <code>UpscalerTest</code> als auch im eigentlichen Spielprojekt mit vorhandenen DLSS-, FSR- und XeSS-Plugins validiert. Auf dem Hybrid-Laptop blieb der Support trotz zusätzlicher Intel UHD korrekt <code>AVAILABLE</code>, weil der aktive Renderadapter statt der bloßen Existenz eines Intel-Adapters ausgewertet wird. | |||
==== Gemessene Kosten ==== | |||
Eine bewusst schwere Wald-/Wasserszene auf der RTX 5060 Laptop GPU wurde bei Epic Scalability und 83,4 % Render Resolution, ungefähr 1404×715 Pixel im Viewport, gemessen. Die Ergebnisse sind Momentaufnahmen und kein allgemeiner Leistungswert. | |||
{| class="wikitable" | |||
! Zustand | |||
! FPS | |||
! Framezeit | |||
! GPU-Zeit | |||
! Zusätzliche GPU-Zeit gegenüber aus | |||
|- | |||
| Neural Rendering aus | |||
| 41,68 | |||
| 23,99 ms | |||
| 17,19 ms | |||
| – | |||
|- | |||
| 1 Pass | |||
| 30,87 | |||
| 32,38 ms | |||
| 26,75 ms | |||
| +9,56 ms | |||
|- | |||
| 2 Passes | |||
| 23,16 | |||
| 43,05 ms | |||
| 35,84 ms | |||
| +18,65 ms | |||
|- | |||
| 4 Passes | |||
| 16,07 | |||
| 62,27 ms | |||
| 54,19 ms | |||
| +37,00 ms | |||
|- | |||
| 2 Passes mit Depth/Motion | |||
| 23,52 | |||
| 42,39 ms | |||
| 35,99 ms | |||
| +18,80 ms | |||
|} | |||
In dieser frühen Testreihe kostete ein Neural-Pass ungefähr 9 bis 9,5 ms zusätzliche GPU-Zeit. Depth und Motion erhöhten den Zwei-Pass-Wert nur um etwa 0,15 ms und lagen damit im Bereich der Messschwankung. Spätere Läufe mit Preset 3 und vier Passes lagen deutlich niedriger, beispielsweise bei ungefähr 26 ms GPU-Zeit in einer anderen Szeneneinstellung. Preset, Feature-Vertrag, Szene und Viewport müssen deshalb bei künftigen Benchmarks immer mitprotokolliert werden. Gerade der große, klar abgrenzbare Evaluierungsblock macht Neural Rendering zu einem interessanten späteren Scheduler-Workload. | |||
== Test- und Diagnosebefehle == | |||
Die vorhandenen Compute-, Texture-, Raster-, StaticMesh-, World- und QueueProbe-Kommandos bleiben bestehen. Neu hinzugekommen ist: | |||
{| class="wikitable" | |||
! Befehl | |||
! Zweck | |||
|- | |||
| <code>ExperimentalMultiGPU.ListWorkloads</code> | |||
| Listet aktive Workloads mit ID, Typ, gewünschter Zuweisung, Priorität, Auflösung, Zielrate, Lifecycle-Statistik und Worker-GPU-Timing auf. | |||
|} | |||
Für den aktuellen Hauptpfad sind außerdem besonders relevant: | |||
<pre> | <pre> | ||
ExperimentalMultiGPU.SetSceneCaptureResolution <Width> <Height> | |||
ExperimentalMultiGPU.GetSceneCaptureResolution | |||
ExperimentalMultiGPU.TestSceneCaptureStaticMeshScene | |||
ExperimentalMultiGPU.StartSceneCaptureStaticMeshSceneStream [Hz] [MaxSuccessfulSubmits] [quiet] | |||
ExperimentalMultiGPU.StopSceneCaptureStaticMeshSceneStream | |||
ExperimentalMultiGPU.TestSkeletalMeshWorker | |||
ExperimentalMultiGPU.ListWorkloads | |||
</pre> | </pre> | ||
Erst | === Texture Stream === | ||
Grundsyntax: | |||
<pre>ExperimentalMultiGPU.StartTextureStream [Hz] [MaxSuccessfulSubmits] [quiet]</pre> | |||
Die Hz-Angabe ist eine Ziel- beziehungsweise Request-Frequenz. <code>MaxSuccessfulSubmits</code> zählt nur angenommene Submits; Busy- und Failed-Versuche verbrauchen das Limit nicht. <code>quiet</code> unterdrückt Per-Job- und Busy-Logspam. | |||
=== QueueProbe === | |||
Grundsyntax: | |||
<pre>ExperimentalMultiGPU.StartQueueProbe <Hz> <MaxSubmits> [quiet] <Mode> <Queue></pre> | |||
Die Diagnosemodi <code>full</code>, <code>waitonly</code>, <code>signalonly</code>, <code>commandonly</code>, <code>barrieronly</code>, <code>barrierprivate</code> und <code>copybuffer</code> bleiben verfügbar. Als Queue können <code>graphics</code>, <code>copy</code>, <code>native</code>, <code>worker</code>, <code>worker-copy</code> und <code>worker-high</code> gewählt werden. Private native Queues dienen weiterhin nur der Diagnose und bilden keinen fertigen Experimental-/Direct-Modus. | |||
=== Benchmark-CVars === | |||
<code>r.VSync 0</code> deaktiviert VSync. <code>t.MaxFPS 30</code>, <code>60</code> oder <code>120</code> erzeugt reproduzierbare Frame-Coupling-Tests; <code>t.MaxFPS 0</code> entfernt das normale Framerate-Limit. | |||
== Aktueller nächster Schritt == | |||
Als Nächstes wird die Source-Engine auf dem Desktop-System mit AMD RX 6900 XT und Intel Arc validiert. DLSS5 soll dort sauber <code>UNSUPPORTED</code> melden und das normale Rendering nicht beeinflussen. Parallel wird die GPU-Zeit des Transfers vom Worker-ColorTarget in den Shared Cross-Adapter Buffer separat gemessen; danach folgt das Timing der Primary-SafeCopy. Erst aus den getrennten Blöcken Worker-Raster, Cross-Adapter-Transfer und Primary-Integration soll ein belastbares Kostenmodell V1 entstehen. Dieses bildet anschließend die Grundlage für automatische Primary-/Worker-Zuweisungen; die aktuelle Registry plant bewusst noch nichts selbst. | |||
== Historische Vorläufer == | |||
Die Grundidee heterogener Multi-GPU-Nutzung ist nicht neu. Bereits mit der Einführung von Direct3D 12 entstanden mehrere Versuche, integrierte und dedizierte GPUs gemeinsam zu verwenden. Eine allgemein einsetzbare Lösung für moderne UE5-Spiele hat sich daraus jedoch nicht entwickelt. | |||
=== Microsoft und Epic: UE4 Elemental Demo (2015) === | |||
Microsoft zeigte eine angepasste Unreal-Engine-4-Version der Elemental-Demo, bei der eine NVIDIA-GPU und eine Intel-iGPU gemeinsam arbeiteten. Das erklärte Ziel war, ansonsten ungenutzte Grafikleistung in Hybrid-Systemen zu verwenden. Dieser Versuch ist der Grundidee des Projekts besonders ähnlich, blieb aber eine technische Demonstration. | |||
=== Intel: D3D12 Multi-Adapter Sample (2015) === | |||
Intels Beispiel kombinierte eine GeForce 840M mit Intel HD Graphics 5500. Beide GPUs berechneten getrennte Teile einer einfach parallelisierbaren Raytracing-Szene. Cross-Adapter-Ressourcen und Shared Fences führten die Ergebnisse anschließend zusammen. Intel berichtete in diesem Test von nahezu halbierter Framezeit, warnte jedoch ausdrücklich davor, das Ergebnis unmittelbar auf reale Spiele zu übertragen. | |||
=== Ashes of the Singularity === | |||
Die Nitrous Engine von ''Ashes of the Singularity'' setzte Direct3D-12-Explicit-Multiadapter in einem veröffentlichten Spiel ein. Dabei konnten auch unterschiedliche GPU-Modelle und Hersteller zusammenarbeiten. Das war ein wichtiger Praxisnachweis, blieb in der Spielebranche aber eine seltene Ausnahme. | |||
=== Weitere verwandte Ansätze === | |||
* ''Rise of the Tomb Raider'' erhielt explizite Direct3D-12-Multi-GPU-Unterstützung, vor allem für klassische Kombinationen ähnlicher GPUs. | |||
* NVIDIA VR SLI wies bei Virtual Reality jeder GPU ein Auge zu. Das verteilt unabhängige Ansichten, bleibt jedoch NVIDIA-spezifisch. | |||
* Unreal nDisplay kann Viewports oder Frustums auf weitere GPUs beziehungsweise Prozesse auslagern und das fertige Bild zur Ausgabe-GPU zurückführen. Der Einsatzbereich ist hauptsächlich Virtual Production. | |||
* Forschung und High-Performance Computing verwenden seit Jahren Scheduler für heterogene GPUs, allerdings überwiegend für Compute- und Datenverarbeitungsaufgaben statt für einen allgemeinen UE5-Echtzeitrenderer. | |||
=== Abgrenzung dieses Projekts === | |||
Das Projekt erfindet Direct3D-12-Explicit-Multiadapter nicht neu. Sein eigener Ansatz liegt in der Kombination aus tiefer UE5.8-RHI-Integration, UE-kompilierten Shadern auf unabhängigen Worker-Devices, frei wählbaren Spiel- und Compute-Workloads, Capability- und Performance-Scheduler, mehreren Fallback-Pfaden sowie dem langfristigen Ziel beliebiger Hersteller und mehrerer Worker. | |||
== Vergleich mit anderen Multi-GPU-Verfahren == | == Vergleich mit anderen Multi-GPU-Verfahren == | ||
| Zeile 150: | Zeile 579: | ||
Worker-Gewinn > Vorbereitung + Datentransfer + Synchronisation + Rückintegration | Worker-Gewinn > Vorbereitung + Datentransfer + Synchronisation + Rückintegration | ||
</pre> | </pre> | ||
== Bekannte Grenzen des Proof of Concepts == | |||
* Die sichtbare Normal-/SafeCopy-/Ready-Kette bleibt an Unreals Submission- und Frame-System gekoppelt. Für sichtbare Outputs ist das akzeptabel; hochfrequente Hintergrundjobs benötigen weitere Architekturtests. | |||
* Pro Worker existiert absichtlich nur ein persistenter sichtbarer Runtime-Slot. Mehrere parallele In-Flight-Jobs und Ringbuffer fehlen noch. | |||
* Praktisch getestet ist Primary plus ein Worker. Mehrere Worker sind architektonisch vorgesehen, aber noch nicht praktisch validiert. | |||
* World- und SceneCapture-Pfade verarbeiten pro Snapshot höchstens acht sichtbare <code>UStaticMeshComponent</code>s. | |||
* Der Static-Mesh-Pfad verwendet LOD0-Positionen, Normalen und Indices ohne UE-Materialien, Texturen, Beleuchtung, Schatten, Nanite, Lumen oder Culling. Für Cooked/Shipping wird ein eigener Geometrie-Cache oder Build-Schritt benötigt. | |||
* <code>USkeletalMesh</code> ist bislang ein LOD0-ReferencePose-Proof. Bone-Skinning, Animation, Morph Targets und Cloth fehlen. | |||
* Die Workload-Registry sammelt Metadaten und Messwerte, führt aber noch keine automatische Zuweisung, Auflösungsänderung oder Ratenanpassung aus. | |||
* Worker GPU Timing V1 misst nur den Rasterabschnitt. Cross-Adapter-Transfer und Primary-SafeCopy fehlen noch im Kostenmodell. | |||
* Lifecycle-Raten können im Editor durch Fensterfokus und Background-Throttling verfälscht werden. Für Kostenentscheidungen sind echte GPU-Timestamps und geglättete Mehrfachmessungen maßgeblich. | |||
* QueueProbe und private native Queues bleiben Diagnosepfade; Experimental/Direct ist noch nicht implementiert. | |||
* Raw-D3D12-Shared-Ressourcen sind noch nicht vollständig in Unreals Residency-Management eingebunden. Device Lost, Timeouts, Speicherdruck und die vollständige Fallback-Matrix müssen vor produktiver Nutzung gehärtet werden. | |||
* DLSS5 Neural Rendering ist aktuell auf Win64, D3D12 und einen aktiven NVIDIA-Renderadapter beschränkt. Der Workload läuft noch auf der Primary-GPU und ist nicht an den heterogenen Worker-Pfad angebunden. | |||
* Depth- und Motion-Guides funktionieren technisch; Bildqualität und temporale Stabilität in Bewegung benötigen weitere Bewertung. | |||
* Direktes Umschalten von <code>r.DLSS5.PassCount</code> war im Editor zeitweise nicht zuverlässig, während der Settings-Slider den Wert korrekt setzte. | |||
* Die Kosten von Neural Rendering hängen stark von Preset, Passzahl, Szene, Feature-Vertrag und Auflösung ab. Einzelmessungen dürfen nicht als feste Kosten pro Pass verallgemeinert werden. | |||
* Linux/Vulkan mit getrennten Devices, External Memory und externer Synchronisation ist noch nicht implementiert. | |||
== Roadmap == | == Roadmap == | ||
# | # Cross-Adapter Transfer Timing V1: Worker-ColorTarget → Shared Buffer separat messen. | ||
# | # Primary SafeCopy Timing ergänzen und die Integrationskosten getrennt halten. | ||
# | # Kostenmodell V1 aus Worker-Raster, Transfer und Primary-Integration pro Workload ableiten. | ||
# | # D3D12-Pfade mit Intel Arc A750/A380 und RX 6900 XT in unterschiedlichen Primary-/Worker-Rollen gegenprüfen; DLSS5 muss auf AMD/Intel sauber zurückfallen. | ||
# | # Weitere echte Workload-Klassen wie CCTV, Spiegel, Minimap und Compute anbinden. | ||
# | # Neural Rendering als groben Worker-Workload untersuchen: zuerst Datenpfad, Ressourcenbedarf und Synchronisation modellieren, danach erst den Offload implementieren. | ||
# | # DLSS5-Benchmarks nach Preset, Passzahl, Scene Guides, Szene und Auflösung standardisieren; direktes <code>PassCount</code>-CVar-Verhalten vereinheitlichen. | ||
# Linux/Vulkan mit getrennten Devices, External Memory und | # Scheduler und Autotuner auf GPU-Eignung und gemessene Nettokosten statt Auslastungsprozent aufbauen. | ||
# Material-, Textur- und Culling-Pfade für realistischere Worker-Szenen ergänzen. | |||
# Skeletal Skinning mit Bone-Matrizen und Skin Weights erst bei einem echten animierten Worker-Workload ausbauen. | |||
# Einen softwareseitigen Multi-Tier Resource Manager für Primary-VRAM, Worker-VRAM, RAM und später Storage untersuchen. | |||
# Experimental-/Direct-Fast-Path nur mit State-Tracking, Residency, Hazards und sicherem Fallback entwickeln. | |||
# Linux/Vulkan mit getrennten Devices, External Memory und externen Semaphoren/Fences untersuchen. | |||
== Git-Checkpoint == | |||
Der aktuelle Engine-Checkpoint <code>1fc0d24</code> trägt die Nachricht <code>Add DLSS5 NR integration and packaged shader support</code> und folgt auf <code>37c6d02</code> (<code>Extend worker capture and workload instrumentation</code>). Der Diff umfasst 29 Dateien mit 4037 Einfügungen und 281 Löschungen. Versioniert wurden gezielt relevante Source-, Build- und Plugin-Dateien; <code>Intermediate</code>, <code>Saved</code>, DDC sowie EXE-, DLL-, LIB- und PDB-Dateien und die proprietäre <code>nvngx_dlssnr.dll</code> blieben ausgeschlossen. Nach dem Commit bestanden keine ausstehenden versionierten Änderungen. | |||
== Quellen == | == Quellen == | ||
* Interner Entwicklungsstand und verifizierte Testprotokolle vom | * Interner Entwicklungsstand und verifizierte Testprotokolle vom 17. September 2026; Commit <code>1fc0d24</code> auf Branch <code>mgpu-experimental</code>. Neu belegt sind cook-fähige ExperimentalMultiGPU-Global-Shader, DLSS5-NR-Integration, Packaged-Runtime, Caller-Gate-Bridge, Scene Guides, Support-Fallback und Performance-Messungen im eigentlichen Spielprojekt. | ||
* [https://learn.microsoft.com/en-us/windows/win32/direct3d12/multi-engine Microsoft: Direct3D 12 Multi-adapter systems] | * [https://learn.microsoft.com/en-us/windows/win32/direct3d12/multi-engine Microsoft: Direct3D 12 Multi-adapter systems] | ||
* [https://learn.microsoft.com/en-us/windows/win32/direct3d12/shared-heaps Microsoft: Shared heaps] | * [https://learn.microsoft.com/en-us/windows/win32/direct3d12/shared-heaps Microsoft: Shared heaps] | ||
* [https://learn.microsoft.com/en-us/windows/win32/direct3d12/timing Microsoft: Timing und Timestamp Queries in Direct3D 12] | |||
* [https://learn.microsoft.com/en-us/samples/microsoft/directx-graphics-samples/d3d12-heterogeneous-multiadapter-sample-win32/ Microsoft: D3D12 Heterogeneous Multiadapter Sample] | * [https://learn.microsoft.com/en-us/samples/microsoft/directx-graphics-samples/d3d12-heterogeneous-multiadapter-sample-win32/ Microsoft: D3D12 Heterogeneous Multiadapter Sample] | ||
* [https://devblogs.microsoft.com/directx/directx-12-multiadapter-lighting-up-dormant-silicon-and-making-it-work-for-you/ Microsoft: UE4 Elemental Demo mit heterogenem Multiadapter] | |||
* [https://www.intel.com/content/dam/develop/external/us/en/documents/multi-adapter-support-in-directx-12-594015.pdf Intel: Multi-Adapter Support in DirectX 12] | |||
* [https://devblogs.microsoft.com/directx/ashes-of-the-singularity-makes-gaming-history-with-directx-12/ Microsoft: Ashes of the Singularity und heterogene Adapter] | |||
* [https://dev.epicgames.com/documentation/en-us/unreal-engine/nvidia-sli-alternative-frame-rendering-in-unreal-engine Epic: NVIDIA SLI Alternate Frame Rendering] | * [https://dev.epicgames.com/documentation/en-us/unreal-engine/nvidia-sli-alternative-frame-rendering-in-unreal-engine Epic: NVIDIA SLI Alternate Frame Rendering] | ||
* [https://dev.epicgames.com/documentation/en-us/unreal-engine/multi-process-rendering-with-unreal-engine Epic: Multi-Process Rendering] | * [https://dev.epicgames.com/documentation/en-us/unreal-engine/multi-process-rendering-with-unreal-engine Epic: Multi-Process Rendering] | ||
Aktuelle Version vom 17. September 2026, 19:37 Uhr
Hinweis: Projektstand: 17. September 2026. Aktueller Engine-Checkpoint: 1fc0d24 (Add DLSS5 NR integration and packaged shader support) auf Branch mgpu-experimental. Der vorherige Checkpoint 37c6d02 bleibt die Grundlage des Worker-, Workload- und GPU-Timing-Pfads. Neu sind cook-fähige ExperimentalMultiGPU-Global-Shader sowie die Integration von DLSS5ForUE5 als realer Neural-Rendering-Workload. Die beschriebenen Ergebnisse sind ein experimenteller Proof of Concept für Unreal Engine 5.8 unter Windows und Direct3D 12.
UE5 Heterogeneous Multi-GPU ist ein experimentelles Projekt zur gemeinsamen Nutzung unterschiedlicher Grafikkarten in Unreal Engine 5. Eine leistungsfähige GPU übernimmt das normale Rendering. Weitere NVIDIA-, AMD- oder Intel-GPUs sollen als unabhängige Worker klar abgegrenzte Aufgaben bearbeiten.
Das Ziel ist ausdrücklich kein klassisches SLI oder CrossFire. Die Anwendung verteilt Arbeit selbst und tauscht nur benötigte Ergebnisse zwischen den GPUs aus. Der Proof of Concept besitzt inzwischen einen durchgängigen Worker-Rasterpfad aus echten UE-Weltdaten bis zur zweiten physischen GPU und zurück in eine persistente UE-Textur. Eine backend-neutrale Workload-Schicht erfasst Aufgaben, Lifecycle-Daten und echte Worker-GPU-Zeiten; automatische Scheduling-Entscheidungen trifft sie noch nicht. Seit Commit 1fc0d24 funktioniert der ExperimentalMultiGPU-Shaderpfad außerdem in Development-Packages. Parallel liefert DLSS5 Neural Rendering einen ersten realen, klar messbaren Kandidaten für eine spätere grobe Worker-Auslagerung; aktuell läuft dieser Workload noch vollständig auf der Primary-NVIDIA-GPU.
| Engine / Plattform | Unreal Engine 5.8 Source Build / Windows / Direct3D 12 |
|---|---|
| Testsystem | Lenovo LOQ 17IRX10 |
| Primary | NVIDIA GeForce RTX 5060 Laptop GPU |
| Worker | Intel UHD Graphics |
| Zusätzliche Testhardware | Intel Arc A750 und Arc A380 als zusätzliche dGPU-Worker-Gegenchecks vorgesehen; später außerdem RX 6900 XT ↔ Intel |
| Git-Stand | Commit 1fc0d24 auf Branch mgpu-experimental; vorheriger Checkpoint 37c6d02; Working Tree nach dem Commit ohne ausstehende versionierte Änderungen
|
| Windows/D3D12-Proof-of-Concept | Durchgängiger sichtbarer Worker-Rasterpfad von der laufenden UE-Welt über getaggte Static und Skeletal Meshes sowie eine echte SceneCapture2D-Kamera bis zur persistenten UE-Textur; dynamisch bis 3840×2160 und 4K60 mit 300/300 Ready-Frames nachgewiesen |
| Mess- und Workload-Schicht | Generische Workload-Registry, Runtime Metrics V1 und echte D3D12-Timestamp-Messungen des Worker-Rasterabschnitts implementiert |
| Packaged Build | Alle neun ExperimentalMultiGPU-Global-Shader nach RenderCore verlagert, beim Cook registriert und im Development-Package erfolgreich gestartet |
| Neural Rendering | DLSS5ForUE5 in Source-Engine, Editor und Packaged Game integriert; Project-Config, native Caller-Gate-Bridge, Depth-/Motion-Guides und robuster Support-Fallback validiert |
| Gesamtvision | etwa 30–35 % (Kostenmodell, Scheduler, weitere echte Workloads, N-GPU und Vulkan noch offen) |
Grundprinzip
Intel Worker GPU -> MainTextureCS + PatternPhase -> persistente Worker-Textur -> persistenter Shared Cross-Adapter Buffer -> monotone Shared Fence / Queue Wait -> persistente UE-eigene FTextureRHIRef -> stabile UExperimentalMultiGPUTexture RTX Primary GPU -> normales UE5-Rendering -> TPS-Material zeigt das Worker-Ergebnis sichtbar an
Pro Worker ist maximal ein Runtime-Job gleichzeitig in flight. Backpressure lehnt weitere Requests als Busy ab, bis der Slot nach der Primary-Completion wieder sicher verwendbar ist.
Jeder Worker besitzt ein eigenes ID3D12Device, eigene Queues, Command Lists und Ressourcen. Ein späterer Scheduler soll Rechenleistung, Fähigkeiten, Transferkosten und gewünschte Aktualisierungsrate bewerten. Vorgesehene Aufgaben sind beispielsweise SceneCapture2D, Spiegel, CCTV, Minimap, Compute oder Niagara.
Verifizierter Entwicklungsstand
Worker-Compute
Ein echter, von Unreal kompilierter Global Compute Shader läuft auf der Intel UHD, während Unreal regulär über die RTX rendert. Der Test verdoppelt 64 Ganzzahlen auf dem Worker; 64 von 64 Ergebnissen wurden korrekt verifiziert. Damit ist echte GPU-Arbeit auf einem zweiten, herstellerfremden Device nachgewiesen.
Zusätzlich läuft MainTextureCS auf der Intel-GPU. Der synchrone Debugtest erzeugt eine 64 × 64 Pixel große RGBA8-Textur; alle 4096 shader-generierten Pixel wurden korrekt verifiziert. Der normale Runtime-Pfad verwendet inzwischen einen asynchronen 128×96-Test. Der Shader liest seine Dimensionen mit GetDimensions(), schützt überhängende Threads durch einen Bounds-Check und wird mit Dispatch(16,12,1) ausgeführt. PatternPhase erzeugt für jeden akzeptierten Submit deterministisch einen neuen sichtbaren Inhalt. Abgewiesene Busy-Requests erhöhen die Phase nicht und erzeugen daher keine Lücken.
Cross-Adapter-Speicher und Synchronisation
- RTX und Intel öffnen denselben D3D12 Cross-Adapter Heap.
- Ein gemeinsamer Buffer wurde mit 64 Testwerten erfolgreich geprüft.
- Ein persistenter Shared Fence synchronisiert die Queues direkt von GPU zu GPU. Seine Werte steigen pro Job monoton: WorkerReady/PrimaryComplete verwenden die Paare 1/2, 3/4, 5/6 und so weiter.
- Die CPU vermittelt die GPU-Abhängigkeit nicht; Worker- und Primary-Queue verwenden
SignalundWaitdirekt auf der GPU. - Der separate Readback-Verifikationstest darf weiterhin blockieren. Der normale Runtime-Pfad wartet dagegen nicht auf GPU-Leerlauf.
Texturtransport
Direkte Cross-Adapter-Texturen sind auf dem Testsystem nicht beidseitig verfügbar: Die Intel UHD meldet Unterstützung für Row-Major Cross-Adapter Textures, die RTX 5060 Laptop GPU nicht. Deshalb verwendet das Projekt einen allgemeineren Buffer-Fallback.
Intel MainTextureCS -> persistente Worker-Textur -> CopyTextureRegion -> persistenter Shared Cross-Adapter Buffer -> Worker signalisiert WorkerReady (1, 3, 5, ...) -> UE-Primary-Queue wartet auf WorkerReady -> direkte Kopie in persistente UE-eigene FTextureRHIRef -> Primary signalisiert PrimaryComplete (2, 4, 6, ...)
Der robuste Normal-Pfad kopiert die Worker-Textur über den Shared Buffer direkt in eine von Unreal erzeugte FTextureRHIRef. Breite, Höhe und Row Pitch werden dynamisch aus Worker-Ressource und Transport-Metadaten übernommen. Der erfolgreiche 128×96-RGBA8-Test verwendet 512 Byte Row Pitch und 49.152 Byte Transfergröße. Die Output Registry trennt Persistent-, Pending- und Ready-Zustände sowie Inhaltsgeneration und Texturidentität. Dadurch kann eine verspätete Completion keinen neueren Output zurückrollen.
Non-blocking Runtime-Pfad
Die produktive Testlogik ist aus dem Konsolenkommando in eine gekapselte NormalPath-API verschoben. SubmitWorkerTextureComputeJob() reicht den Worker-Dispatch ohne CPU-Wait ein. QueueNormalTextureTransfer() reiht Wait, Kopie und Completion-Signal in Unreals D3D12-Kontext ein. Der normale MainTextureCS-Pfad enthält weder CPU-Fence-Waits noch Readback, Map oder Pixelvergleich. TickNormalTextureTransfers() prüft die Primary-Completion non-blocking über einen Runtime-Pump; RHIEndFrame() bleibt als Fallback erhalten.
Eine passende Primary-FTextureRHIRef wird pro Worker und Konfiguration persistent gehalten. Nach PrimaryComplete kann der nächste Job dieselbe Resource erneut verwenden; dabei führt Unreal die Übergänge SRVMask -> CopyDest -> SRVMask aus. Eine backend-neutrale RHI-Provider-API reicht die Ready-Textur an die Engine weiter. UExperimentalMultiGPUTexture hält eine stabile UTexture samt TextureReference und bindet die persistente FTextureRHIRef ohne zusätzlichen Pixel-Copy. Eine native D3D12-Resource wird weiterhin nicht blind als UE-Textur gewrappt, weil Unreals Resource-State-Buchhaltung den tatsächlichen nativen Zustand sonst nicht sicher kennt.
Sichtbarer TPS-Consumer
AExperimentalMultiGPUDisplayActor zeigt die Worker-Ausgabe im Third-Person-Testlevel über den Materialparameter MultiGPUTexture an. Damit ist der Output nicht mehr nur über Logs oder Readback nachgewiesen: Die Intel UHD erzeugt fortlaufend die Texturinhalte, während die RTX die Spielwelt und das Material rendert. TextureConsumerCS kann Ready-Ausgaben auf der Primary zusätzlich als SRV lesen und fünf quantisierte Samples im Debugpfad prüfen.
Lebensdauer und automatisches Cleanup
Der Normal-Pfad verwendet pro Worker genau einen persistenten Runtime-Slot. Worker-Textur, Root Signature, PSO, Descriptor Heap, Command Allocator/List und Worker-Fence werden nach der ersten kompatiblen Initialisierung wiederverwendet. Dasselbe gilt für Shared Heap/Buffer/Fence und die Transport-Command-Objekte. Der In-Flight-Job trägt nur noch die nötigen Job-, Generation- und Fence-Metadaten.
Runtime Request -> Completion non-blocking prüfen -> falls Slot frei: Worker Dispatch -> WorkerReady-Fence signalisieren -> Primary Wait, SafeCopy und PrimaryComplete-Signal -> Output Pending -> Runtime-Pump oder RHIEndFrame prüft GetCompletedValue() -> Output Ready publizieren -> Slot wiederverwendbar
Backpressure erlaubt maximal einen echten In-Flight-Job pro Worker. Ein Busy-Request setzt keine GPU-Arbeit ab, setzt keine Ressourcen zurück und erhöht PatternPhase nicht. Worker-Fence-Werte steigen mit jedem akzeptierten Submit 1, 2, 3 und so weiter; der persistente Shared Fence verwendet fortlaufend die Paare 1/2, 3/4, 5/6. Fehler nach Übergabe von Ressourcen an die GPU führen zu sicherem Retire beziehungsweise Retention statt zu vorzeitigem Reuse.
Texture Stream und Benchmarking
Der projektseitige Texture Stream akzeptiert eine beliebige positive, endliche Zielrate. Punkt und Komma sind als Dezimaltrenner möglich. Optional lässt sich die Zahl erfolgreicher Submits begrenzen; Busy- oder Failed-Versuche verbrauchen dieses Limit nicht. Der Quiet-Modus unterdrückt per-Job-Ausgaben, behält aber Start, echte Fehler und die Abschlussstatistik bei.
Die Statistik unterscheidet Timer-Ticks, angenommene Submits, Busy/Failed, Ready, In-Flight und die jeweiligen effektiven Raten. Diagnosezeitpunkte erfassen unter anderem RequestAccepted, RenderCommandExecution, WorkerSubmit, NormalTransferQueued und ReadyObserved.
| Test | Ergebnis | Aussage |
|---|---|---|
| 5 Hz, 20 Submits | etwa 5 Ready/s, 20/20, kein Busy | Entspannter Single-Slot-Betrieb |
| 1000 Hz, 100 Submits, quiet | 100/100 Ready, 0 Fehler, etwa 58 Ready/s | Backpressure begrenzt den Durchsatz sicher |
1000 Hz bei t.MaxFPS 30
|
etwa 28–29 Ready/s trotz rund 982 Timer-Ticks/s | Primary-/SafeCopy-/Ready-Kette ist effektiv framegekoppelt |
Ein zusätzlicher non-blocking Aufruf von ImmediateFlush(DispatchToRHIThread) erreicht hohe Render-Command-Raten, hebt die gemessene Frame-Kopplung aber nicht auf. Im 30-FPS-Test lag der RHIEndFrame-spezifische Completion-Takt praktisch exakt bei 30 Hz. Das aktuelle Problem ist damit eingegrenzt: Nicht der Stream-Timer, sondern die Primary-RHI-/D3D12-Submission der SafeCopy-Kette bestimmt die sichtbare Ready-Rate.
Queue-Submission-Diagnose
MultiGPUD3D12QueueProbe untersucht die Submission unabhängig vom sichtbaren Texturpfad. Neben full, waitonly und signalonly existieren inzwischen die Diagnosemodi computeonly, commandonly, barrieronly, barrierprivate und copybuffer. Als Queues stehen graphics, copy, native, worker, worker-copy und worker-high zur Verfügung. Graphics und Copy verwenden RHIRunOnQueue(..., false) auf UE-verwalteten Primary-Queues; die übrigen Varianten isolieren private beziehungsweise bereits vorhandene native D3D12-Queues.
Der Probe misst Request, Worker-Submit, RHIRunOnQueue-Aufruf und -Callback, Primary-Execute, Fence-Completion sowie Ready bis zum nächsten akzeptierten Submit. Er fügt keine CPU-Waits, Readbacks, Maps oder BlockUntilGPUIdle ein und beschreibt keine Textur des sichtbaren Normal-Pfads.
| Test | Ergebnis | Befund |
|---|---|---|
| UE Graphics, signal-only, 30 FPS | 33,319 ms PrimaryExecute bis Ready | UE-verwaltete Graphics-Submission skaliert mit der Frame-Dauer |
| UE Copy, signal-only, 30 FPS | 32,593 ms PrimaryExecute bis Ready | Auch die UE-verwaltete Copy Queue ist framegekoppelt |
| Private Primary-DIRECT-Queue, signal-only | etwa 0,014 ms Queue/Fence-Latenz; nach Lifecycle-Fix rund 999 Ready/s bei 30 FPS | Native Queue und Scheduler sind vollständig vom Frame entkoppelt |
| Worker signal-only / leere Command List | jeweils rund 1000 Ready/s bei 30 FPS | Worker-Queue, Fence und reine Submission sind nicht framegekoppelt |
Worker CopyBufferRegion, 30 / 60 / 120 FPS
|
29,41 / 58,25 / 113,08 Ready/s | Bereits eine echte 256-Byte-GPU-Kopie folgt im UE-Prozess dem Frame-Raster |
| Worker COPY Queue / HIGH Priority, 30 FPS | jeweils rund 29,42 Ready/s | Queue-Typ und Priorität beseitigen die Kopplung nicht |
| Standalone-D3D12 auf derselben Intel UHD | 92.956 Copy-Jobs/s; 0,007 ms durchschnittliche Fence-Latenz | Die Kopplung ist UE-Prozess-/Scheduling-spezifisch, keine allgemeine Intel-, WDDM- oder D3D12-Grenze |
Der RHIRunOnQueue-Callback selbst erreicht den D3D12-Submission-Thread typischerweise bereits nach etwa 0,02–0,05 ms; die Verzögerung entsteht danach in der verwalteten Queue-/Submission-Reihenfolge. SubmitCommandsHint() ist in UE 5.8 nur ein veralteter Alias für ImmediateFlush(DispatchToRHIThread) und stellt daher keinen stärkeren Submission-Mechanismus dar.
Der native Test ist ein Architekturbeweis, noch kein Experimental- oder Direct-Modus. Die zunächst gemessene Verzögerung von rund 32 ms lag zwischen der In-Flight-Freigabe und dem nächsten Request auf dem Render Thread. Für signalonly/native wurde der sichere Completion-/Submit-Lifecycle deshalb in den frameunabhängigen Scheduler verlegt. Danach erreichten zwei 100-Job-Läufe bei t.MaxFPS 30 rund 999 bis 1029 Ready/s, ohne Busy oder Fehler.
Der anschließende full native-Test lokalisierte die verbleibende Kopplung auf echte Worker-GPU-Arbeit: WorkerSubmit bis WorkerReady benötigte bei 30, 60 und 120 FPS ungefähr 33,29, 16,93 und 8,45 ms; WorkerReady bis PrimaryReady war danach praktisch sofort. Signal-only und eine leere Command List waren schnell, Resource Barriers und eine 256-Byte-Kopie dagegen framegebunden. Ein eigenständiger D3D12-Microbenchmark auf derselben Intel UHD erreichte außerhalb von Unreal bei derselben Kopie rund 92.956 Jobs/s mit 0,007 ms mittlerer Fence-Latenz. Die genaue Ursache im UE-Prozess beziehungsweise dessen GPU-Scheduling-Kontext bleibt offen und wird vorerst bis zum Gegencheck mit der vorhandenen Intel Arc A380 geparkt.
Worker-Rasterpfad und RasterStream
ExperimentalMultiGPU.TestRasterWorker weist erstmals echte Rasterizer-Arbeit auf der Intel-Worker-GPU nach. RasterWorkerVS und RasterWorkerPS werden über Unreals Global-Shader-Pfad kompiliert und für ein natives Worker-Graphics-PSO verwendet. Ein persistentes 128×96-RGBA8-RenderTarget besitzt RTV, leere Root Signature und eine DIRECT-Command-List. Der Vertex Shader erzeugt drei Vertices über SV_VertexID; der Pixel Shader schreibt interpolierte RGB-Farben.
Der Job wechselt das RenderTarget von COPY_SOURCE nach RENDER_TARGET, führt Clear und DrawInstanced(3,1,0,0) aus und wechselt anschließend zurück nach COPY_SOURCE. Danach verwendet er unverändert den robusten Normalpfad aus Cross-Adapter-Shared-Buffer, Primary SafeCopy, persistenter FTextureRHIRef, UExperimentalMultiGPUTexture und DisplayActor. Der Runtime-Test meldete einen akzeptierten Submit und RasterWorker READY für Generation 1; die 128×96-UTexture wurde erfolgreich gebunden.
Der einmalige Test wurde außerdem zu einem frei konfigurierbaren RasterStream ausgebaut. Ein sichtbarer Lauf mit 10 Hz und 50 erfolgreichen Submits endete mit 50 Ready-Ausgaben, 0 Busy und 0 Fehlern bei etwa 9,90 Ready/s. Primary-Textur und Materialbindung blieben dabei persistent.
Raster3D mit echten Vertex- und Indexbuffern
Der Worker besitzt inzwischen einen kleinen eigenständigen 3D-Rasterpfad mit perspektivischer Kamera, Model-View-Projection-Matrix als Root Constants, D32_FLOAT-Depth-Buffer und Depth-Test. Der finale Testwürfel verwendet 24 POSITION/COLOR-Vertices, 36 uint16-Indices und DrawIndexedInstanced(36,1,0,0,0); sechs getrennte Seitenfarben machen Ausrichtung und Rotation sichtbar nachvollziehbar.
Die statischen Meshdaten werden einmalig über private Upload-Buffer in DEFAULT-Heaps übertragen. Der Init-Fence wird non-blocking über GetCompletedValue() beobachtet. Der eigentliche Runtime-Renderpfad enthält weiterhin keine CPU-Waits und keine Maps.
UE-Static-Meshes auf der Worker-GPU
StaticMeshWorker extrahiert echte LOD0-Geometrie aus UStaticMesh-Assets. POSITION und NORMAL sowie uint32-Indices werden in POD-Daten überführt, einmalig asynchron in eigene Worker-VB/IB hochgeladen und anschließend per DrawIndexedInstanced gerendert. Assetpfade sind dynamisch; Auto-Fit richtet Kamera beziehungsweise Modellmaßstab an den Bounds aus.
Sichtbar korrekt getestet wurden:
| Asset | Vertices | Indices |
|---|---|---|
| Sphere | 559 | 2880 |
| Cube | 54 | 144 |
| Cylinder | 334 | 1536 |
Multi-Mesh Worker-Szene
StaticMeshScene rendert Sphere, Cube und Cylinder gemeinsam in einen Color-/Depth-Frame. Der verifizierte Test umfasst insgesamt 947 Vertices und 4560 Indices, drei DrawIndexedInstanced-Aufrufe und nur einen anschließenden Cross-Adapter-/Normal-SafeCopy-Transfer. Damit ist nicht mehr nur ein synthetisches Primitive, sondern mehrere echte UE-Meshes mit gemeinsamer Tiefenprüfung im Worker-Frame sichtbar nachgewiesen.
WorldStaticMeshScene
ExperimentalMultiGPUWorldStaticMeshScene.cpp sammelt im Engine-Layer bis zu acht sichtbare UStaticMeshComponents von Actors mit dem Tag MultiGPUWorker. Meshdaten und World-Transforms werden im Game Thread in reine POD-Strukturen und 4×4-Matrizen kopiert. D3D12RHI erhält keine UObject-Pointer. Mehrere Instanzen desselben Meshes teilen den persistenten Worker-Mesh-Cache und unterscheiden sich nur durch Modelmatrix und Draw Call.
Der Pfad wurde inzwischen sichtbar im PIE-Level verifiziert. Drei getaggte Static-Mesh-Actors erschienen mit ihren tatsächlichen relativen Positionen, Rotationen und Skalierungen im Worker-Bild. Die zentrale Koordinatenumrechnung lautet:
Worker (X, Y, Z) = (UE Y, UE X, -UE Z)
Die Transformation besitzt die Determinante +1 und ist damit keine Spiegelung. Das notwendige horizontale Spiegeln erfolgt ausschließlich auf der Presentation-Plane, nicht im Worker-Renderpfad.
Kontinuierlicher WorldStaticMeshSceneStream
StartWorldStaticMeshSceneStream erfasst die getaggte Szene fortlaufend im Game Thread. Reine Änderungen an Transform oder Kamera lösen keinen erneuten Mesh-Upload aus. Hinzugekommene, entfernte, unsichtbare oder umgetaggte Components werden im nächsten Snapshot berücksichtigt. Single-Slot-Backpressure verhindert einen wachsenden Rückstau.
Ein verifizierter 30-Hz-Lauf endete nach 31,184 Sekunden mit 835 Ticks, 691 angenommenen Submits, 144 Busy, 0 Fehlern und 691 Ready-Frames. Die effektive Ready-Rate betrug 22,16 Hz. Dieser Test belegt die funktionale Live-Aktualisierung; die Rate war noch kein Optimierungsziel.
SceneCapture2D als Worker-Kamera
TestSceneCaptureStaticMeshScene verwendet eine echte ASceneCapture2D beziehungsweise USceneCaptureComponent2D aus der laufenden PIE-Welt als Kameraquelle. Der empfohlene Editor-Weg ist das Component Tag MultiGPUWorkerCamera auf der CaptureComponent2D; ein gleichnamiges Actor-Tag bleibt nur als Legacy-Fallback.
Ausgelesen werden World Location, Rotation, Forward/Right/Up, FOVAngle und ProjectionType. Orthografische Projektion wird derzeit sauber abgelehnt. Die SceneCapture2D liefert ausschließlich Kameradaten: Ihr UE-RenderTarget wird weder gelesen noch kopiert. Die Intel-Worker-GPU rendert die Geometrie selbst. Kamera und Actors werden gemeinsam mit SceneScale = 0.01 in Worker-Koordinaten überführt; Auto-Fit und Scene-Recentering sind in diesem Modus deaktiviert.
Die 128×96-Worker-Projektion verwendet ein Seitenverhältnis von 4:3 und interpretiert FOVAngle als horizontales FOV. Nach den Orientierungstests wird nur die Anzeige über SetRelativeScale3D(FVector(-4.0f, -3.0f, 1.0f)) korrigiert; dies entspricht U' = 1-U und V' = 1-V.
Live SceneCaptureStaticMeshSceneStream
StartSceneCaptureStaticMeshSceneStream liest Kamera-Transform, Kamerabasis, FOV und aktuelle Actor-Transforms pro Snapshot neu im Game Thread. Der Worker erhält weiterhin ausschließlich POD-Daten, verwendet den persistenten Mesh-Cache sowie ein gemeinsames Color- und D32-Depth-Target und führt pro akzeptiertem Frame genau einen Cross-Adapter-/Normal-SafeCopy-Transfer aus.
Der Quiet-Test
ExperimentalMultiGPU.StartSceneCaptureStaticMeshSceneStream 30 300 quiet
lieferte in 10,084 Sekunden 301 Ticks, 300 Submits, 1 Busy, 0 Fehler und 300 Ready-Frames. Die effektive Ready-Rate betrug 29,75 Hz. Bewegungen der Kamera und Actors sowie FOV-Änderungen erschienen live auf der Display-Plane. Damit ist erstmals ein kleiner SceneCapture-/CCTV-artiger Worker-Kanal vollständig funktional nachgewiesen.
Dynamische SceneCapture-Auflösung
Die SceneCapture-Ausgabe ist nicht mehr fest auf 128×96 Pixel begrenzt. ExperimentalMultiGPU.SetSceneCaptureResolution <Width> <Height> setzt ganzzahlige Dimensionen von 1 bis 4096; GetSceneCaptureResolution zeigt die aktive Einstellung. 128×96 bleibt der Standard. Während ein SceneCapture-Stream läuft, wird ein Auflösungswechsel absichtlich abgelehnt, damit keine In-Flight-Ressourcen ausgetauscht werden.
Color Target, D32-Depth-Buffer, Row Pitch, Shared-Buffer-Transport, SafeCopy, persistente Primary-Textur und UExperimentalMultiGPUTexture übernehmen die gewählten Dimensionen. Projektion und Display-Plane verwenden das echte Seitenverhältnis. Die bestätigte Anzeigeorientierung U' = 1-U und V' = 1-V bleibt eine reine Presentation-Korrektur; Worker-Basis und Kameramathematik ändern sich nicht.
Scheduler-Korrektur bei SceneCapture
Ein 1920×1080-Test mit 25 Hz erreichte zunächst trotz fehlerfreier Frames nur 13,90 Ready-Hz und erzeugte 222 Busy-Ticks. Ursache war kein GPU-Limit, sondern ein Polling-Aliasing: Eine frühe Game-Thread-Prüfung konnte den Timer-Slot verwerfen, obwohl die Completion kurz darauf im CoreTicker/RHI-Pump sichtbar geworden wäre.
Die frühe IsRuntimeTextureUpdateInFlight()-Prüfung wurde für diesen Stream entfernt. Der Pending-Request bleibt als Backpressure-Gate erhalten; die maßgebliche Busy-Entscheidung fällt nun erst im Render Command nach TickNormalTextureTransfers(nullptr, true). Single-Slot, POD-Snapshots und non-blocking GetCompletedValue() bleiben erhalten. Es wurden weder CPU-Waits noch zusätzliche Threads eingeführt.
Nach der Korrektur erreicht 1080p bei 25 Hz 24,79 Ready-Hz mit 1 Busy; bei 30 Hz sind es 29,43 Ready-Hz mit 4 Busy.
Skalierung bis 4K60
Die Pipeline wurde von 640×360 bis 3840×2160 getestet. Bei 640×360/30 Hz und 1280×720/30 Hz wurden 29,85 beziehungsweise 29,82 Ready-Hz ohne Busy erreicht. 1920×1080/60 Hz lieferte 59,40 Ready-Hz ohne Busy, 2560×1440/60 Hz 59,21 Ready-Hz mit 1 Busy und 3840×2160/60 Hz 59,02 Ready-Hz mit 2 Busy.
Ein zusätzlicher 4K60-Stresstest mit acht Static-Mesh-Instanzen und drei gecachten Unique Meshes erreichte in 5,049 Sekunden 300 von 300 Ready-Frames, 0 Busy, 0 Fehler und 59,42 effektive Ready-Hz. Acht Draw Calls, gemeinsamer Depth Buffer, Cross-Adapter-Transport und SafeCopy liefen dabei im vorhandenen Single-Slot. Das ist ein Stabilitätsnachweis der aktuellen Mini-Szene, kein allgemeiner GPU-Benchmark.
Skeletal Mesh in Reference Pose
ExperimentalMultiGPU.TestSkeletalMeshWorker erweitert den Geometriepfad auf USkeletalMeshComponent. Der Engine-Layer sucht deterministisch die erste sichtbare, getaggte Komponente und extrahiert LOD0-Positionen, Normalen, Indices und Render Sections. D3D12RHI erhält wie bisher ausschließlich POD-Geometrie und Modelmatrizen.
Der sichtbare Test mit SKM_Quinn_Simple rendert Quinn in der Reference Pose mit 45.993 Vertices, 261.840 Indices, zwei Sections und zwei Draw Calls. Die beiden Sections umfassen 35.612 und 51.668 Dreiecke. Worker-VB/IB, gemeinsamer D32-Depth-Buffer, dynamische Auflösung, Cross-Adapter-Transport und Normal/SafeCopy werden wiederverwendet.
Eine Reference Pose ist die unverformte Ausgangshaltung des Skeletal Meshes. Bone-Matrizen, Skin Weights, laufende Animationen, Morph Targets und Cloth sind noch nicht angebunden. Skeletal Skinning ist erst vorgesehen, wenn ein echter Worker-Workload wie CCTV oder Spiegel animierte Figuren in seiner eigenen Ansicht benötigt.
Generische Workload-Registry
Commit 37c6d02 führt erstmals eine backend-neutrale Workload-Schicht ein. Sie beschreibt Aufgaben unabhängig vom konkreten Grafik-Backend und bildet die Grundlage für einen späteren Scheduler, trifft selbst aber noch keine GPU-Entscheidung.
| Bestandteil | Inhalt |
|---|---|
| Workload-Typ | Unknown, SceneCapture, Compute oder Raster
|
| Gewünschte Zuweisung | Primary, Worker oder Auto; Auto ist derzeit nur Metadatum
|
| Priorität | Low, Normal oder High
|
| Descriptor | Sessionweit monotone ID, Typ, Zuweisung, Priorität, Auflösung, Zielrate, Debugname und Running-Status |
Die Registry stellt RegisterWorkload, UpdateWorkload, UnregisterWorkload, GetWorkload und GetActiveWorkloads bereit und schützt ihre Daten per FCriticalSection. Descriptoren enthalten weder UObject- noch D3D12-Pointer. Der SceneCapture-Stream registriert genau einen Worker-Workload mit normaler Priorität und entfernt ihn bei Stop oder Auto-Stop wieder vollständig.
ExperimentalMultiGPU.ListWorkloads zeigt die aktiven Descriptoren und ihre Laufzeitwerte an.
Runtime Metrics V1
Descriptor und Laufzeitstatistik sind getrennt. Pro Workload werden Ticks, angenommene Submits, Busy- und Fehlerfälle, fertige Frames, aktuell ausstehende Jobs, Laufzeit sowie effektive Tick-, Submit- und Ready-Raten erfasst. Der SceneCapture-Pfad spiegelt dafür seine vorhandenen Stream-Zähler in die Registry, statt eine zweite Statistiklogik aufzubauen. Die Messwerte werden derzeit nur beobachtet und ändern weder Zuweisung noch Auflösung oder Zielrate.
Editor-Fokus und Background-Throttling können die Lifecycle-Raten deutlich verfälschen. In einem sauberen 1280×720-Referenzlauf wurden 1000/1000 Ready-Frames ohne Busy bei 29,84 Ready-Hz gemessen; bei wechselndem Fensterfokus entstanden dagegen 459 Busy-Ticks und nur 16,69 Ready-Hz, ohne dass die Worker-GPU-Zeit entsprechend einbrach. Solche Raten sind daher kein verlässlicher Ersatz für echte GPU-Kostenmessungen.
Worker GPU Timing V1
Der SceneCapture-Worker verwendet persistente native D3D12-Timestamp-Queries auf seiner vorhandenen DIRECT-Queue: einen Query Heap mit zwei Slots, einen kleinen Readback-Buffer und die per GetTimestampFrequency() ermittelte Zeitbasis. Das Ergebnis wird erst nach non-blocking geprüftem Fence-Abschluss gelesen; CPU-Waits entstehen nicht. Die Workload-ID wird als POD bis zum Worker mitgeführt, während einmalige Tests die ID 0 verwenden.
Gemessen wird ausschließlich der Worker-Rasterabschnitt:
Start Timestamp -> COPY_SOURCE nach RENDER_TARGET -> Color- und Depth-Clear -> alle DrawIndexedInstanced-Aufrufe -> RENDER_TARGET nach COPY_SOURCE End Timestamp -> ResolveQueryData
Nicht enthalten sind der Cross-Adapter-Transfer, die Primary-SafeCopy und die Integration auf der Primary-GPU. Erfasst werden Sample-Anzahl sowie letzter, mittlerer, minimaler und maximaler Messwert.
| Auflösung | Samples | Mittelwert | Minimum | Maximum |
|---|---|---|---|---|
| 1280×720 | 845 | 0,166 ms | 0,165 ms | 0,181 ms |
| 3840×2160 | 599 | 0,683 ms | 0,594 ms | 0,751 ms |
Bei identischem, bewusst einfachem Szeneninhalt mit sieben Draw Calls steigt die gemessene Worker-Rasterzeit damit ungefähr um Faktor 4,1. Die Pixelzahl allein erklärt die Skalierung nicht vollständig. Für ein belastbares Kostenmodell müssen als Nächstes Transfer und Primary-Integration separat gemessen werden.
Cook- und Packaged-Support der Multi-GPU-Shader
Vor Commit 1fc0d24 startete ein Packaged Build nicht, weil unter anderem die Permutation von FExperimentalMultiGPUStaticMeshPS in der cooked Global Shader Map fehlte. Die Shader selbst waren korrekt; ihre neun FGlobalShader-Typen wurden jedoch im D3D12RHI registriert, das beim Cook zu diesem Zeitpunkt noch nicht geladen war.
Die Deklarationen liegen nun im RenderCore-Header ExperimentalMultiGPUShaders.h, die zugehörigen IMPLEMENT_GLOBAL_SHADER-Registrierungen in ExperimentalMultiGPUShaders.cpp. D3D12RHI verwendet nur noch die exportierten RenderCore-Typen. Shaderpfade, Entry Points und die bestehende Shader-Blob-Erkennung bleiben unverändert. Der Cook kompilierte die ExperimentalMultiGPU-Shader nachweislich; das anschließende Development-Package startete ohne den früheren Missing global shader-Fatal. Damit ist der Worker-Pfad shaderseitig nicht mehr auf den Editor beschränkt.
DLSS5 Neural Rendering als späterer Worker-Kandidat
Der Branch enthält nun auch DLSS5ForUE5 als quelltextbasierte Win64-/D3D12-Integration für NVIDIA Neural Rendering. Das Plugin verwendet den bereits vom offiziellen NVIDIA-DLSS-Plugin initialisierten NGX-Core und eröffnet keine konkurrierende NGX-Application-Session. Neural Rendering läuft derzeit auf der Primary-NVIDIA-GPU; ein Multi-GPU-Offload ist noch nicht implementiert.
Die Integration umfasst:
- eine eigene
Config/DLSS5ForUE5.inimit 20 projektbezogenen Einstellungen, - einen ausschließlich im Editor wirksamen Safe Boot, der Neural Rendering und Scene Guides nur für die aktuelle Session deaktiviert,
- eine native
DLSS5ForUE5_nvngx.dllals unmittelbaren Caller für den NVIDIA-Caller-Gate, - Depth- und Motion-Guides aus der laufenden UE-Szene,
- die Supportzustände
AVAILABLE,UNAVAILABLE,UNSUPPORTEDundWAITINGmit sauberem Fallback statt wiederholtem Init-Logspam.
Im monolithischen Packaged Game wurde der NGX-Init zunächst mit 0xBAD00002 abgelehnt, weil der Aufruf aus der Spiel-EXE kam. Eine native Bridge löst dieses Problem. Zusätzlich musste die MSVC-Tail-Call-Optimierung verhindert werden: Erst ein echter call mit erhaltenem Bridge-Stackframe ließ die DLL als unmittelbaren Caller sichtbar bleiben. Danach meldete die Runtime Caller Gate, NGX-Core, Parameter und Initialisierung erfolgreich. In einem Packaged-Lauf waren 2036 von 2036 Evaluierungen erfolgreich; Depth und Motion waren jeweils angefordert, verfügbar und verwendet.
Der Pfad wurde sowohl im kleinen UpscalerTest als auch im eigentlichen Spielprojekt mit vorhandenen DLSS-, FSR- und XeSS-Plugins validiert. Auf dem Hybrid-Laptop blieb der Support trotz zusätzlicher Intel UHD korrekt AVAILABLE, weil der aktive Renderadapter statt der bloßen Existenz eines Intel-Adapters ausgewertet wird.
Gemessene Kosten
Eine bewusst schwere Wald-/Wasserszene auf der RTX 5060 Laptop GPU wurde bei Epic Scalability und 83,4 % Render Resolution, ungefähr 1404×715 Pixel im Viewport, gemessen. Die Ergebnisse sind Momentaufnahmen und kein allgemeiner Leistungswert.
| Zustand | FPS | Framezeit | GPU-Zeit | Zusätzliche GPU-Zeit gegenüber aus |
|---|---|---|---|---|
| Neural Rendering aus | 41,68 | 23,99 ms | 17,19 ms | – |
| 1 Pass | 30,87 | 32,38 ms | 26,75 ms | +9,56 ms |
| 2 Passes | 23,16 | 43,05 ms | 35,84 ms | +18,65 ms |
| 4 Passes | 16,07 | 62,27 ms | 54,19 ms | +37,00 ms |
| 2 Passes mit Depth/Motion | 23,52 | 42,39 ms | 35,99 ms | +18,80 ms |
In dieser frühen Testreihe kostete ein Neural-Pass ungefähr 9 bis 9,5 ms zusätzliche GPU-Zeit. Depth und Motion erhöhten den Zwei-Pass-Wert nur um etwa 0,15 ms und lagen damit im Bereich der Messschwankung. Spätere Läufe mit Preset 3 und vier Passes lagen deutlich niedriger, beispielsweise bei ungefähr 26 ms GPU-Zeit in einer anderen Szeneneinstellung. Preset, Feature-Vertrag, Szene und Viewport müssen deshalb bei künftigen Benchmarks immer mitprotokolliert werden. Gerade der große, klar abgrenzbare Evaluierungsblock macht Neural Rendering zu einem interessanten späteren Scheduler-Workload.
Test- und Diagnosebefehle
Die vorhandenen Compute-, Texture-, Raster-, StaticMesh-, World- und QueueProbe-Kommandos bleiben bestehen. Neu hinzugekommen ist:
| Befehl | Zweck |
|---|---|
ExperimentalMultiGPU.ListWorkloads
|
Listet aktive Workloads mit ID, Typ, gewünschter Zuweisung, Priorität, Auflösung, Zielrate, Lifecycle-Statistik und Worker-GPU-Timing auf. |
Für den aktuellen Hauptpfad sind außerdem besonders relevant:
ExperimentalMultiGPU.SetSceneCaptureResolution <Width> <Height> ExperimentalMultiGPU.GetSceneCaptureResolution ExperimentalMultiGPU.TestSceneCaptureStaticMeshScene ExperimentalMultiGPU.StartSceneCaptureStaticMeshSceneStream [Hz] [MaxSuccessfulSubmits] [quiet] ExperimentalMultiGPU.StopSceneCaptureStaticMeshSceneStream ExperimentalMultiGPU.TestSkeletalMeshWorker ExperimentalMultiGPU.ListWorkloads
Texture Stream
Grundsyntax:
ExperimentalMultiGPU.StartTextureStream [Hz] [MaxSuccessfulSubmits] [quiet]
Die Hz-Angabe ist eine Ziel- beziehungsweise Request-Frequenz. MaxSuccessfulSubmits zählt nur angenommene Submits; Busy- und Failed-Versuche verbrauchen das Limit nicht. quiet unterdrückt Per-Job- und Busy-Logspam.
QueueProbe
Grundsyntax:
ExperimentalMultiGPU.StartQueueProbe <Hz> <MaxSubmits> [quiet] <Mode> <Queue>
Die Diagnosemodi full, waitonly, signalonly, commandonly, barrieronly, barrierprivate und copybuffer bleiben verfügbar. Als Queue können graphics, copy, native, worker, worker-copy und worker-high gewählt werden. Private native Queues dienen weiterhin nur der Diagnose und bilden keinen fertigen Experimental-/Direct-Modus.
Benchmark-CVars
r.VSync 0 deaktiviert VSync. t.MaxFPS 30, 60 oder 120 erzeugt reproduzierbare Frame-Coupling-Tests; t.MaxFPS 0 entfernt das normale Framerate-Limit.
Aktueller nächster Schritt
Als Nächstes wird die Source-Engine auf dem Desktop-System mit AMD RX 6900 XT und Intel Arc validiert. DLSS5 soll dort sauber UNSUPPORTED melden und das normale Rendering nicht beeinflussen. Parallel wird die GPU-Zeit des Transfers vom Worker-ColorTarget in den Shared Cross-Adapter Buffer separat gemessen; danach folgt das Timing der Primary-SafeCopy. Erst aus den getrennten Blöcken Worker-Raster, Cross-Adapter-Transfer und Primary-Integration soll ein belastbares Kostenmodell V1 entstehen. Dieses bildet anschließend die Grundlage für automatische Primary-/Worker-Zuweisungen; die aktuelle Registry plant bewusst noch nichts selbst.
Historische Vorläufer
Die Grundidee heterogener Multi-GPU-Nutzung ist nicht neu. Bereits mit der Einführung von Direct3D 12 entstanden mehrere Versuche, integrierte und dedizierte GPUs gemeinsam zu verwenden. Eine allgemein einsetzbare Lösung für moderne UE5-Spiele hat sich daraus jedoch nicht entwickelt.
Microsoft und Epic: UE4 Elemental Demo (2015)
Microsoft zeigte eine angepasste Unreal-Engine-4-Version der Elemental-Demo, bei der eine NVIDIA-GPU und eine Intel-iGPU gemeinsam arbeiteten. Das erklärte Ziel war, ansonsten ungenutzte Grafikleistung in Hybrid-Systemen zu verwenden. Dieser Versuch ist der Grundidee des Projekts besonders ähnlich, blieb aber eine technische Demonstration.
Intel: D3D12 Multi-Adapter Sample (2015)
Intels Beispiel kombinierte eine GeForce 840M mit Intel HD Graphics 5500. Beide GPUs berechneten getrennte Teile einer einfach parallelisierbaren Raytracing-Szene. Cross-Adapter-Ressourcen und Shared Fences führten die Ergebnisse anschließend zusammen. Intel berichtete in diesem Test von nahezu halbierter Framezeit, warnte jedoch ausdrücklich davor, das Ergebnis unmittelbar auf reale Spiele zu übertragen.
Ashes of the Singularity
Die Nitrous Engine von Ashes of the Singularity setzte Direct3D-12-Explicit-Multiadapter in einem veröffentlichten Spiel ein. Dabei konnten auch unterschiedliche GPU-Modelle und Hersteller zusammenarbeiten. Das war ein wichtiger Praxisnachweis, blieb in der Spielebranche aber eine seltene Ausnahme.
Weitere verwandte Ansätze
- Rise of the Tomb Raider erhielt explizite Direct3D-12-Multi-GPU-Unterstützung, vor allem für klassische Kombinationen ähnlicher GPUs.
- NVIDIA VR SLI wies bei Virtual Reality jeder GPU ein Auge zu. Das verteilt unabhängige Ansichten, bleibt jedoch NVIDIA-spezifisch.
- Unreal nDisplay kann Viewports oder Frustums auf weitere GPUs beziehungsweise Prozesse auslagern und das fertige Bild zur Ausgabe-GPU zurückführen. Der Einsatzbereich ist hauptsächlich Virtual Production.
- Forschung und High-Performance Computing verwenden seit Jahren Scheduler für heterogene GPUs, allerdings überwiegend für Compute- und Datenverarbeitungsaufgaben statt für einen allgemeinen UE5-Echtzeitrenderer.
Abgrenzung dieses Projekts
Das Projekt erfindet Direct3D-12-Explicit-Multiadapter nicht neu. Sein eigener Ansatz liegt in der Kombination aus tiefer UE5.8-RHI-Integration, UE-kompilierten Shadern auf unabhängigen Worker-Devices, frei wählbaren Spiel- und Compute-Workloads, Capability- und Performance-Scheduler, mehreren Fallback-Pfaden sowie dem langfristigen Ziel beliebiger Hersteller und mehrerer Worker.
Vergleich mit anderen Multi-GPU-Verfahren
| Verfahren | Arbeitsweise | Verhältnis zu diesem Projekt |
|---|---|---|
| SLI / CrossFire | Treiber- beziehungsweise Verbundlösung für meist ähnliche GPUs; häufig wird die Bildarbeit verteilt. | Dieses Projekt benötigt keinen herstellerspezifischen GPU-Verbund und weist Aufgaben ausdrücklich selbst zu. |
| AFR (Alternate Frame Rendering) | GPU 1 rendert einen Frame, GPU 2 den nächsten. | Das Projekt verteilt unabhängige Aufgaben statt aufeinanderfolgender Frames. Dadurch werden Frame-Abhängigkeiten und typisches AFR-Pacing vermieden. |
| SFR (Split Frame Rendering) | Mehrere GPUs bearbeiten Bereiche desselben Frames. | Erfordert enge Lastverteilung und viel Datenaustausch. Das Projekt bevorzugt vollständig abgrenzbare Workloads und überträgt deren Ergebnis. |
| Direct3D 12 Linked Multiadapter | Mehrere vom Treiber verbundene GPUs erscheinen als Knoten eines logischen Adapters. | Eignet sich eher für eng kompatible GPUs. Das Projekt verwendet unabhängige Devices (Unlinked/Explicit Multiadapter) und unterstützt dadurch heterogene Herstellerkombinationen. |
| Direct3D 12 Unlinked / Explicit Multiadapter | Die Anwendung verwaltet getrennte Adapter, Ressourcen und Synchronisation selbst. | Das ist die technische Familie des Projekts. Hinzu kommen die eigene UE5-RHI-Integration, Workload-Ziele, Fallbacks und der geplante Scheduler. |
| UE nDisplay mGPU / Multi-Process | Separate GPUs rendern bestimmte Viewports oder Frustums, vor allem für Virtual Production; Ergebnisse werden zur Ausgabe-GPU kopiert. | Ähnliche Idee der aufgabenweisen Trennung, aber für einen anderen Einsatzbereich. Dieses Projekt zielt auf allgemeine Spiel- und Compute-Workloads innerhalb der Engine. |
| Vulkan Device Groups | Ähnliche physische GPUs können ein gemeinsames logisches Device bilden. | Für beliebige heterogene GPUs meist ungeeignet, da Geräte einer Gruppe nahezu gleiche Eigenschaften besitzen müssen. Eine spätere Vulkan-Portierung benötigt daher wahrscheinlich getrennte Devices und External Memory/Semaphores. |
SLI und CrossFire bezeichnen den Verbund; AFR und SFR beschreiben mögliche Verteilungsmethoden innerhalb solcher Systeme. Sie sind deshalb nicht vollständig getrennte Kategorien.
Vorteile der geplanten Variante
- Herstellerunabhängig: NVIDIA, AMD und Intel können grundsätzlich kombiniert werden.
- Vorhandene Hardware nutzen: Auch eine sonst wenig genutzte integrierte GPU kann geeignete Nebenaufgaben übernehmen.
- Aufgaben statt Frames verteilen: Spiegel, Minimap oder Compute können mit eigener Auflösung und Aktualisierungsrate laufen.
- Keine identischen GPUs erforderlich: Unterschiedliche Fähigkeiten können gezielt genutzt werden.
- Kontrollierter Datenaustausch: Nur das benötigte Ergebnis muss zurück zur Primary-GPU.
- Robuste Fallback-Idee: Wenn direkte Texturen nicht gemeinsam nutzbar sind, bleibt der Shared-Buffer-Pfad.
- Erweiterbar: Scheduler, manuelles Mapping und mehrere Worker sind als spätere Stufen vorgesehen.
Nachteile und technische Risiken
- Hoher Entwicklungsaufwand: Geräte, Ressourcen, Zustände, Fences, Fehlerfälle und UE-Lebenszyklen müssen selbst verwaltet werden.
- Transfer kann den Gewinn aufzehren: Cross-Adapter Heaps liegen laut D3D12 nicht automatisch im schnellen lokalen VRAM. Bandbreite und Latenz müssen für jeden Workload gemessen werden.
- VRAM wird nicht einfach addiert: Benötigte Ressourcen können auf mehreren GPUs vorliegen und zusätzlichen Speicher verbrauchen.
- Langsame Worker können bremsen: Eine Aufgabe lohnt sich nur, wenn Rechengewinn größer als Übergabe-, Warte- und Kopierkosten ist.
- Nicht jeder Workload ist unabhängig: Hauptansicht, Lumen, Nanite und stark gekoppelte Renderpässe besitzen viele Abhängigkeiten und sind schwieriger auszulagern.
- Hardwareunterschiede: Formate, Shader-Funktionen, Queue-Fähigkeiten und Cross-Adapter-Support müssen pro GPU geprüft werden.
- Wartungsrisiko: Eingriffe in private D3D12RHI-Dateien können bei Engine-Updates angepasst werden müssen.
- Produktionsreife fehlt noch: Ressourcenpools, Timeouts, Device-Lost-Wiederherstellung, Scheduler und echte Szenentests sind offen.
Wann die Variante sinnvoll ist
Am aussichtsreichsten sind Aufgaben mit viel eigener Rechenarbeit, einem relativ kleinen Endergebnis und wenigen Abhängigkeiten zur Hauptszene. Gute frühe Kandidaten sind unabhängige Compute-Aufgaben sowie Scene Captures, die nicht in jedem Frame aktualisiert werden müssen. Weniger geeignet sind winzige Aufgaben mit großem Transfer oder Renderpässe, die ständig auf Ressourcen der Primary-GPU zugreifen.
Die entscheidende Regel für den späteren Scheduler lautet daher:
Worker-Gewinn > Vorbereitung + Datentransfer + Synchronisation + Rückintegration
Bekannte Grenzen des Proof of Concepts
- Die sichtbare Normal-/SafeCopy-/Ready-Kette bleibt an Unreals Submission- und Frame-System gekoppelt. Für sichtbare Outputs ist das akzeptabel; hochfrequente Hintergrundjobs benötigen weitere Architekturtests.
- Pro Worker existiert absichtlich nur ein persistenter sichtbarer Runtime-Slot. Mehrere parallele In-Flight-Jobs und Ringbuffer fehlen noch.
- Praktisch getestet ist Primary plus ein Worker. Mehrere Worker sind architektonisch vorgesehen, aber noch nicht praktisch validiert.
- World- und SceneCapture-Pfade verarbeiten pro Snapshot höchstens acht sichtbare
UStaticMeshComponents. - Der Static-Mesh-Pfad verwendet LOD0-Positionen, Normalen und Indices ohne UE-Materialien, Texturen, Beleuchtung, Schatten, Nanite, Lumen oder Culling. Für Cooked/Shipping wird ein eigener Geometrie-Cache oder Build-Schritt benötigt.
USkeletalMeshist bislang ein LOD0-ReferencePose-Proof. Bone-Skinning, Animation, Morph Targets und Cloth fehlen.- Die Workload-Registry sammelt Metadaten und Messwerte, führt aber noch keine automatische Zuweisung, Auflösungsänderung oder Ratenanpassung aus.
- Worker GPU Timing V1 misst nur den Rasterabschnitt. Cross-Adapter-Transfer und Primary-SafeCopy fehlen noch im Kostenmodell.
- Lifecycle-Raten können im Editor durch Fensterfokus und Background-Throttling verfälscht werden. Für Kostenentscheidungen sind echte GPU-Timestamps und geglättete Mehrfachmessungen maßgeblich.
- QueueProbe und private native Queues bleiben Diagnosepfade; Experimental/Direct ist noch nicht implementiert.
- Raw-D3D12-Shared-Ressourcen sind noch nicht vollständig in Unreals Residency-Management eingebunden. Device Lost, Timeouts, Speicherdruck und die vollständige Fallback-Matrix müssen vor produktiver Nutzung gehärtet werden.
- DLSS5 Neural Rendering ist aktuell auf Win64, D3D12 und einen aktiven NVIDIA-Renderadapter beschränkt. Der Workload läuft noch auf der Primary-GPU und ist nicht an den heterogenen Worker-Pfad angebunden.
- Depth- und Motion-Guides funktionieren technisch; Bildqualität und temporale Stabilität in Bewegung benötigen weitere Bewertung.
- Direktes Umschalten von
r.DLSS5.PassCountwar im Editor zeitweise nicht zuverlässig, während der Settings-Slider den Wert korrekt setzte. - Die Kosten von Neural Rendering hängen stark von Preset, Passzahl, Szene, Feature-Vertrag und Auflösung ab. Einzelmessungen dürfen nicht als feste Kosten pro Pass verallgemeinert werden.
- Linux/Vulkan mit getrennten Devices, External Memory und externer Synchronisation ist noch nicht implementiert.
Roadmap
- Cross-Adapter Transfer Timing V1: Worker-ColorTarget → Shared Buffer separat messen.
- Primary SafeCopy Timing ergänzen und die Integrationskosten getrennt halten.
- Kostenmodell V1 aus Worker-Raster, Transfer und Primary-Integration pro Workload ableiten.
- D3D12-Pfade mit Intel Arc A750/A380 und RX 6900 XT in unterschiedlichen Primary-/Worker-Rollen gegenprüfen; DLSS5 muss auf AMD/Intel sauber zurückfallen.
- Weitere echte Workload-Klassen wie CCTV, Spiegel, Minimap und Compute anbinden.
- Neural Rendering als groben Worker-Workload untersuchen: zuerst Datenpfad, Ressourcenbedarf und Synchronisation modellieren, danach erst den Offload implementieren.
- DLSS5-Benchmarks nach Preset, Passzahl, Scene Guides, Szene und Auflösung standardisieren; direktes
PassCount-CVar-Verhalten vereinheitlichen. - Scheduler und Autotuner auf GPU-Eignung und gemessene Nettokosten statt Auslastungsprozent aufbauen.
- Material-, Textur- und Culling-Pfade für realistischere Worker-Szenen ergänzen.
- Skeletal Skinning mit Bone-Matrizen und Skin Weights erst bei einem echten animierten Worker-Workload ausbauen.
- Einen softwareseitigen Multi-Tier Resource Manager für Primary-VRAM, Worker-VRAM, RAM und später Storage untersuchen.
- Experimental-/Direct-Fast-Path nur mit State-Tracking, Residency, Hazards und sicherem Fallback entwickeln.
- Linux/Vulkan mit getrennten Devices, External Memory und externen Semaphoren/Fences untersuchen.
Git-Checkpoint
Der aktuelle Engine-Checkpoint 1fc0d24 trägt die Nachricht Add DLSS5 NR integration and packaged shader support und folgt auf 37c6d02 (Extend worker capture and workload instrumentation). Der Diff umfasst 29 Dateien mit 4037 Einfügungen und 281 Löschungen. Versioniert wurden gezielt relevante Source-, Build- und Plugin-Dateien; Intermediate, Saved, DDC sowie EXE-, DLL-, LIB- und PDB-Dateien und die proprietäre nvngx_dlssnr.dll blieben ausgeschlossen. Nach dem Commit bestanden keine ausstehenden versionierten Änderungen.
Quellen
- Interner Entwicklungsstand und verifizierte Testprotokolle vom 17. September 2026; Commit
1fc0d24auf Branchmgpu-experimental. Neu belegt sind cook-fähige ExperimentalMultiGPU-Global-Shader, DLSS5-NR-Integration, Packaged-Runtime, Caller-Gate-Bridge, Scene Guides, Support-Fallback und Performance-Messungen im eigentlichen Spielprojekt. - Microsoft: Direct3D 12 Multi-adapter systems
- Microsoft: Shared heaps
- Microsoft: Timing und Timestamp Queries in Direct3D 12
- Microsoft: D3D12 Heterogeneous Multiadapter Sample
- Microsoft: UE4 Elemental Demo mit heterogenem Multiadapter
- Intel: Multi-Adapter Support in DirectX 12
- Microsoft: Ashes of the Singularity und heterogene Adapter
- Epic: NVIDIA SLI Alternate Frame Rendering
- Epic: Multi-Process Rendering
- Khronos: Vulkan Specification – Device Groups
