praca_magisterska/latex/tex/5-testy-wydajnosci.tex

1065 lines
52 KiB
TeX

\clearpage
\raggedbottom
\section{Testy wydajności}
\label{sec:testy-wydajnosci}
Dla każdego scenariusza i~silnika rejestrowano następujące metryki przy użyciu NVIDIA Nsight Systems:
\begin{itemize}
\item \textbf{Czas klatki} (frame time) -- czas renderowania pojedynczej klatki w~milisekundach.
\item \textbf{FPS} (frames per second) -- liczba klatek na sekundę, wyliczana jako $1000 / \text{frame time}$.
\item \textbf{Wykorzystanie GPU} -- procent wykorzystania mocy obliczeniowej karty graficznej.
\item \textbf{Zużycie pamięci VRAM} -- ilość zajętej pamięci karty graficznej w~megabajtach.
\item \textbf{Liczba wywołań rysowania} (draw calls) -- liczba instrukcji renderowania na klatkę.
\item \textbf{Liczba wierzchołków} -- całkowita liczba przetworzonych wierzchołków na klatkę.
\end{itemize}
\subsection{Wyniki testów dla silnika Unity}
\label{subsec:wyniki-unity}
Profilowanie silnika Unity przeprowadzono przy użyciu narzędzia NVIDIA Nsight Systems w wersji 2025.5.2,
które umożliwia szczegółową analizę wywołań
API graficznych oraz funkcji systemowych na poziomie pojedynczych mikrosekund. Test trwał 95 sekund,
podczas których gra działała w
trybie stacjonarnym (gracz nieruchomy) z włączoną nieśmiertelnością, co pozwoliło na stabilne pomiary
bez przerwania rozgrywki.
Podczas 94,16-sekundowego okresu aktywnego renderowania zarejestrowano łącznie 13\,556 klatek,
co przekłada się na średnią wydajność
\textbf{143,96 klatek na sekundę} (FPS). Wartość ta niemal dokładnie odpowiada częstotliwości
odświeżania monitora testowego (144 Hz),
co wskazuje na \textbf{włączoną synchronizację pionową} (V-Sync) podczas testu. Oznacza to,
że zmierzona wydajność reprezentuje
górny limit narzucony przez monitor, a nie rzeczywistą maksymalną wydajność silnika Unity.
\begin{table}[H]
\centering
\caption{Ogólne metryki wydajności silnika Unity}
\label{tab:unity-performance-summary}
\begin{tabular}{|l|r|}
\hline
\textbf{Metryka} & \textbf{Wartość} \\
\hline
Czas trwania testu & 94,16 s \\
Liczba wyrenderowanych klatek & 13\,556 \\
Średnia liczba FPS & 143,96 \\
Średni czas klatki & 6,95 ms \\
Minimalny czas klatki & 0,08 ms \\
Maksymalny czas klatki & 1\,239,62 ms \\
Odchylenie standardowe & 10,64 ms \\
Współczynnik zmienności & 153,24\% \\
\hline
\end{tabular}
\end{table}
Tabela~\ref{tab:unity-performance-summary} przedstawia podstawowe metryki wydajności. Średni czas
klatki wynoszący 6,95 ms oznacza, że silnik
Unity jest w stanie wyrenderować pojedynczą klatkę w czasie znacznie krótszym niż wymagane 16,67 ms
dla osiągnięcia 60 FPS. Minimalny czas
klatki 0,08 ms odpowiada sytuacjom, gdy kolejne wywołania prezentacji następują niemal natychmiast po
sobie -- może to wynikać z mechanizmu
podwójnego buforowania (ang. \textit{double buffering}) lub chwilowego braku pracy do wykonania przez GPU.
Wartość maksymalna 1\,239,62 ms (ponad sekunda) występuje podczas fazy inicjalizacji aplikacji,
gdy silnik Unity wykonuje jednorazowe operacje: kompilację shaderów, alokację dużych bloków pamięci \\ GPU,
tworzenie obiektów swapchain oraz
inicjalizację systemu renderowania. Jest to zachowanie typowe dla aplikacji Vulkan, gdzie znaczna część
pracy inicjalizacyjnej wykonywana jest przy
starcie, w przeciwieństwie do OpenGL, gdzie inicjalizacja jest bardziej rozłożona w czasie.
Współczynnik zmienności (CV) wynoszący 153,24\% jest wysoki, jednak wynika on głównie z
uwzględnienia ekstremalnych wartości inicjalizacyjnych.
Po wykluczeniu pierwszych kilku klatek, stabilność renderowania jest znacznie wyższa, co potwierdza
analiza percentylowa przedstawiona w dalszej części.
Szczegółowa analiza rozkładu czasów klatek pozwala ocenić nie tylko średnią wydajność, ale przede
wszystkim stabilność i przewidywalność działania
silnika -- aspekty kluczowe dla komfortu odbiorcy gry.
\begin{table}[H]
\centering
\caption{Rozkład percentylowy czasów klatek silnika Unity}
\label{tab:unity-percentiles}
\begin{tabular}{|l|r|r|}
\hline
\textbf{Percentyl} & \textbf{Czas klatki (ms)} & \textbf{Odpowiadający FPS} \\
\hline
1. percentyl (najszybsze 1\%) & 0,71 & 1\,408 \\
5. percentyl & 6,69 & 149 \\
25. percentyl (Q1) & 6,90 & 145 \\
50. percentyl (mediana) & 6,94 & 144 \\
75. percentyl (Q3) & 6,98 & 143 \\
95. percentyl & 7,18 & 139 \\
99. percentyl (najwolniejsze 1\%) & 7,58 & 132 \\
\hline
\end{tabular}
\end{table}
Tabela~\ref{tab:unity-percentiles} prezentuje rozkład percentylowy czasów klatek. \textbf{Mediana}
(50. percentyl) wynosząca 6,94 ms jest niemal
identyczna z teoretycznym czasem klatki przy 144 Hz (6,944 ms), co potwierdza aktywną
synchronizację pionową. Wąski rozstęp między 5. percentylem
(6,69 ms, 149 FPS) a 95. percentylem (7,18 ms, 139 FPS) -- zaledwie 0,49 ms --
jest charakterystyczny dla V-Sync, gdzie czas klatki jest
sztucznie stabilizowany przez oczekiwanie na sygnał odświeżania monitora.
Szczególnie istotny jest \textbf{99. percentyl} wynoszący 7,58 ms, określany w środowisku graczy
jako ,,1\% low'' (132 FPS). Wartość ta reprezentuje
wydajność w najgorszych 1\% przypadków i jest kluczową metryką dla oceny płynności rozgrywki.
Różnica między medianą (6,94 ms) a 99. percentylem
(7,58 ms) wynosi 0,64 ms (9,2\%). Należy jednak zauważyć, że niska zmienność jest częściowo
wynikiem działania V-Sync, który stabilizuje
czas klatki kosztem wprowadzenia opóźnienia wejścia (ang. \textit{input lag}).
\textbf{Rozstęp międzykwartylowy} (IQR), czyli różnica między 75. a 25. percentylem, wynosi zaledwie
0,08 ms. Tak niski IQR potwierdza, że 50\%
środkowych czasów klatek mieści się w niezwykle wąskim przedziale, co jest oznaką deterministycznego i
przewidywalnego zachowania potoku renderowania.
\begin{table}[H]
\centering
\caption{Histogram czasów klatek silnika Unity}
\label{tab:unity-histogram}
\begin{tabular}{|l|r|r|}
\hline
\textbf{Przedział czasu klatki} & \textbf{Liczba klatek} & \textbf{Udział (\%)} \\
\hline
0--5 ms (>200 FPS) & 230 & 1,70 \\
5--10 ms (100--200 FPS) & 13\,317 & 98,24 \\
10--16,67 ms (60--100 FPS) & 4 & 0,03 \\
16,67--33,33 ms (30--60 FPS) & 2 & 0,01 \\
>33,33 ms (<30 FPS) & 2 & 0,01 \\
\hline
\end{tabular}
\end{table}
Histogram przedstawiony w tabeli~\ref{tab:unity-histogram} dostarcza dodatkowego wglądu w rozkład
wydajności. \textbf{98,24\% wszystkich klatek}
zostało wyrenderowanych w czasie 5--10 ms, co odpowiada wydajności 100--200 FPS. Jedynie 8 klatek
(0,06\%) przekroczyło próg 10 ms, przy czym klatki
poniżej 60 FPS (>16,67 ms) stanowiły zaledwie 0,02\% -- praktycznie wszystkie z nich przypadły na fazę
inicjalizacji.
Kategoria 0--5 ms (230 klatek, 1,70\%) reprezentuje sytuacje szczególne: bardzo szybkie klatki podczas
przejść między scenami, momenty niskiego
obciążenia lub artefakty pomiarowe wynikające z mechanizmu synchronizacji swapchain.
NVIDIA Nsight Systems przechwytuje wszystkie wywołania interfejsu programistycznego Vulkan, umożliwiając dokładną analizę zachowania silnika
renderującego na poziomie pojedynczych funkcji API. Podczas testu zarejestrowano łącznie \textbf{218\,815 wywołań} 31 różnych funkcji Vulkan API.
\begin{table}[H]
\centering
\caption{Wywołania Vulkan API silnika Unity -- funkcje synchronizacji i prezentacji}
\label{tab:unity-vulkan-sync}
\small
\begin{tabular}{|l|r|r|r|r|r|}
\hline
\textbf{Funkcja} & \textbf{Czas (\%)} & \textbf{Wywołania} & \textbf{Śr. (ms)} & \textbf{Med. (ms)} & \textbf{Maks. (ms)} \\
\hline
\texttt{vkWaitForFences} & 95,2 & 12\,895 & 5,97 & 6,23 & 1\,181,17 \\
\texttt{vkQueuePresentKHR} & 3,2 & 13\,556 & 0,19 & 0,02 & 7,20 \\
\texttt{vkQueueSubmit} & 0,8 & 27\,112 & 0,03 & 0,01 & 2,69 \\
\texttt{vkAcquireNextImageKHR} & 0,0 & 13\,556 & 0,001 & 0,001 & 0,11 \\
\texttt{vkQueueWaitIdle} & 0,0 & 1 & 0,27 & 0,27 & 0,27 \\
\hline
\end{tabular}
\end{table}
\texttt{vkWaitForFences} pochłonęła \textbf{95,2\% całkowitego czasu}
profilowania wywołań Vulkan API, co stanowi 77,04 sekundy z
94-sekundowego testu. Funkcja ta, zdefiniowana w specyfikacji Vulkan w rozdziale 7.3 dotyczącym
synchronizacji, realizuje blokujące oczekiwanie
procesora na sygnalizację obiektów ogrodzenia (ang. \textit{fence}) przez GPU.
Mechanizm ogrodzeń w Vulkan działa następująco: aplikacja tworzy obiekt fence,
dołącza go do operacji przesyłanej do kolejki GPU
(np. poprzez \texttt{vkQueueSubmit}), a następnie może wywołać \texttt{vkWaitForFences},
aby zablokować wątek CPU do momentu zakończenia
powiązanej pracy przez GPU. Jest to fundamentalny mechanizm synchronizacji w architekturze
producent-konsument między CPU a GPU.
Tak wysoki udział procentowy (95,2\%) jednoznacznie wskazuje na scenariusz \textbf{ograniczenia wydajności przez GPU} (ang. \textit{GPU-bound}).
W tym scenariuszu procesor główny zakończył przygotowywanie i przesyłanie poleceń renderowania, a następnie oczekuje na ukończenie ich wykonania
przez kartę graficzną. Jest to pożądany wzorzec w dobrze zoptymalizowanych aplikacjach graficznych -- procesor nie stanowi wąskiego gardła i
zdąża przygotować pracę dla GPU przed zakończeniem poprzedniej klatki.
Średni czas pojedynczego wywołania wyniósł 5,97 ms przy medianie 6,23 ms.
Różnica między średnią a medianą (0,26 ms) wynika z obecności bardzo krótkich
czasów oczekiwania w niektórych sytuacjach (np. gdy GPU zakończył pracę przed wywołaniem wait).
Maksymalny czas 1\,181,17 ms odpowiada fazie
inicjalizacji, podczas której GPU wykonuje jednorazowe, kosztowne operacje.
Stosunek liczby wywołań \texttt{vkWaitForFences} (12\,895) do liczby klatek (13\,556) wskazuje, że
Unity stosuje strategię oczekiwania, prawie na
każdą klatkę z pewnymi optymalizacjami pozwalającymi pominąć oczekiwanie w niektórych przypadkach.
\texttt{vkQueuePresentKHR}, zdefiniowana w rozszerzeniu \texttt{VK\_KHR\_swapchain}, odpowiada za przesłanie żądania prezentacji
wyrenderowanego obrazu do silnika prezentacji (ang. \textit{presentation engine}). Każde wywołanie tej funkcji reprezentuje jedną klatkę przekazaną
do wyświetlenia, dlatego liczba wywołań (13\,556) równa jest liczbie wyrenderowanych klatek.
Średni czas wywołania 0,19 ms przy medianie zaledwie 0,02 ms wskazuje na asymetryczny rozkład -- większość wywołań jest bardzo szybka, ale niektóre
wymagają dłuższego oczekiwania (maksymalnie 7,20 ms). Dłuższe czasy mogą wynikać z oczekiwania na dostępność bufora w swapchain lub synchronizacji z
częstotliwością odświeżania monitora (nawet przy wyłączonym V-Sync, pewien poziom synchronizacji jest wymagany).
\texttt{vkQueueSubmit} przesyła bufory poleceń (ang. \textit{command buffers}) do kolejki GPU celem wykonania. Zarejestrowano 27\,112 wywołań, co
oznacza średnio 2 wywołania na klatkę. Taki wzorzec sugeruje, że Unity stosuje architekturę z oddzielnymi przebiegami renderowania
(np. przebieg główny + post-processing lub przebieg sceny + UI).
Niski średni czas (0,03 ms) potwierdza, że \texttt{vkQueueSubmit} jedynie kolejkuje pracę bez oczekiwania na jej wykonanie -- faktyczne renderowanie
odbywa się asynchronicznie na GPU.
\begin{table}[H]
\centering
\caption{Wywołania Vulkan API silnika Unity -- bufory poleceń}
\label{tab:unity-vulkan-cmd}
\small
\begin{tabular}{|l|r|r|r|r|}
\hline
\textbf{Funkcja} & \textbf{Wywołania} & \textbf{Śr. ($\mu$s)} & \textbf{Med. ($\mu$s)} & \textbf{Maks. ($\mu$s)} \\
\hline
\texttt{vkBeginCommandBuffer} & 40\,679 & 2,53 & 1,76 & 2\,049 \\
\texttt{vkEndCommandBuffer} & 40\,679 & 0,73 & 0,63 & 116 \\
\texttt{vkCmdPipelineBarrier} & 40\,800 & 0,46 & 0,39 & 97 \\
\texttt{vkCmdBindPipeline} & 27\,027 & 1,07 & 0,99 & 36 \\
\texttt{vkAllocateCommandBuffers} & 687 & 12,78 & 12,08 & 67 \\
\texttt{vkCreateCommandPool} & 687 & 1,22 & 0,22 & 10 \\
\hline
\end{tabular}
\end{table}
Tabela~\ref{tab:unity-vulkan-cmd} przedstawia statystyki funkcji związanych z buforami poleceń.
Liczba wywołań \texttt{vkBeginCommandBuffer} oraz
\texttt{vkEndCommandBuffer} (po 40\,679) oznacza, że Unity nagrywa średnio 3 bufory poleceń na klatkę. Jest to typowa wartość dla nowoczesnych
silników stosujących wielowątkowe nagrywanie poleceń.
Funkcja \texttt{vkCmdPipelineBarrier} (40\,800 wywołań) służy do synchronizacji dostępu do zasobów w obrębie GPU i zapewnienia poprawnej kolejności
operacji. Wysoka liczba wywołań wskazuje na staranną kontrolę zależności między operacjami renderowania.
\texttt{vkCmdBindPipeline} (27\,027 wywołań, ~2 na klatkę) przełącza aktywny stan potoku graficznego. Relatywnie niska liczba wywołań sugeruje efektywne
grupowanie obiektów według używanego potoku, minimalizując kosztowne zmiany stanu.
\begin{table}[H]
\centering
\caption{Wywołania Vulkan API silnika Unity -- inicjalizacja i zasoby}
\label{tab:unity-vulkan-init}
\begin{tabular}{|l|r|r|r|}
\hline
\textbf{Funkcja} & \textbf{Wywołania} & \textbf{Całk. czas (ms)} & \textbf{Śr. (ms)} \\
\hline
\texttt{vkCreateDevice} & 1 & 162,35 & 162,35 \\
\texttt{vkCreateSwapchainKHR} & 1 & 77,02 & 77,02 \\
\texttt{vkCreateFence} & 341 & 135,60 & 0,40 \\
\texttt{vkAllocateMemory} & 22 & 15,07 & 0,68 \\
\texttt{vkFreeMemory} & 8 & 5,07 & 0,63 \\
\texttt{vkCreateGraphicsPipelines} & 3 & 0,38 & 0,13 \\
\texttt{vkCreateImage} & 106 & 0,24 & 0,002 \\
\texttt{vkCreateImageView} & 111 & 0,17 & 0,002 \\
\texttt{vkCreateShaderModule} & 6 & 0,04 & 0,006 \\
\hline
\end{tabular}
\end{table}
Tabela~\ref{tab:unity-vulkan-init} przedstawia jednorazowe operacje inicjalizacyjne. \texttt{vkCreateDevice} (162,35 ms) tworzy logiczne
urządzenie Vulkan -- jest to najdroższa pojedyncza operacja, obejmująca negocjację możliwości GPU, alokację struktur wewnętrznych sterownika i
inicjalizację kolejek.
\texttt{vkCreateSwapchainKHR} (77,02 ms) tworzy łańcuch wymiany (swapchain), czyli zestaw
buforów służących do prezentacji obrazu. Operacja ta
obejmuje alokację pamięci dla buforów, konfigurację formatów i synchronizację z systemem okienkowym.
Utworzenie 341 obiektów fence (łącznie 135,60 ms) wskazuje na
przygotowanie puli ogrodzeń do wielokrotnego użytku w cyklu renderowania.
Unity stosuje
strategię \\ pre-alokacji zamiast tworzenia ogrodzeń na żądanie, co
jest praktyką zalecaną w dokumentacji
Vulkan.
Oprócz wywołań Vulkan API, Nsight Systems przechwytuje również wywołania funkcji systemowych, umożliwiając analizę zachowania aplikacji na poziomie
systemu operacyjnego. Zarejestrowano \textbf{29\,383 wywołania} 65 różnych funkcji systemowych.
\begin{table}[H]
\centering
\caption{Wywołania systemowe silnika Unity -- synchronizacja wątków}
\label{tab:unity-osrt-sync}
\small
\begin{tabular}{|l|r|r|r|r|r|}
\hline
\textbf{Funkcja} & \textbf{Czas (\%)} & \textbf{Wywołania} & \textbf{Śr. (ms)} & \textbf{Med. ($\mu$s)} & \textbf{Maks. (s)} \\
\hline
\texttt{futex} & 95,9 & 247 & 444,07 & 88,49 & 11,05 \\
\texttt{pthread\_cond\_timedwait} & 2,7 & 85 & 35,91 & 7\,070,65 & 2,00 \\
\texttt{pthread\_cond\_wait} & 0,6 & 26 & 28,59 & 10\,433,06 & 0,47 \\
\texttt{pthread\_create} & 0,0 & 81 & 0,04 & 33,34 & 0,00009 \\
\texttt{pthread\_join} & 0,0 & 3 & 0,11 & 106,59 & 0,00014 \\
\hline
\end{tabular}
\end{table}
Funkcja \texttt{futex} (ang. \textit{Fast Userspace muTEX}) pochłonęła \textbf{95,9\% czasu} wywołań systemowych. Futex jest mechanizmem
synchronizacji wątków w jądrze Linux, zaprojektowanym dla maksymalnej wydajności w scenariuszach bez rywalizacji (ang. \textit{uncontended case}).
Mechanizm futex działa dwuetapowo:
\begin{enumerate}
\item W przypadku braku rywalizacji, operacje na muteksie wykonywane są całkowicie w przestrzeni użytkownika poprzez instrukcje atomowe, bez
przełączania do jądra.
\item Gdy występuje rywalizacja (inny wątek trzyma blokadę), wątek wykonuje wywołanie systemowe \texttt{futex} z operacją \texttt{FUTEX\_WAIT},
które usypia wątek do momentu zwolnienia blokady.
\end{enumerate}
Tak wysoki udział \texttt{futex} (109,69 sekundy łącznie) wskazuje na intensywne wykorzystanie wielowątkowości przez silnik Unity. Silnik ten stosuje
architekturę wielowątkową z oddzielnymi wątkami dla: głównej pętli gry, renderowania, fizyki, audio, wczytywania zasobów oraz systemu zadań
(ang. \textit{job system}).
Średni czas wywołania 444,07 ms przy medianie zaledwie 88,49 $\mu$s wskazuje na silnie asymetryczny rozkład -- większość wywołań kończy się szybko
(wątek od razu uzyskuje blokadę lub jest natychmiast budzony), ale niektóre wywołania skutkują długim oczekiwaniem. Maksymalny czas 11,05 sekundy
odpowiada najprawdopodobniej wątkowi oczekującemu na zakończenie długotrwałej operacji inicjalizacyjnej.
Funkcje \texttt{pthread\_cond\_timedwait} (2,7\%, 85 wywołań) i \texttt{pthread\_cond\_wait} (0,6\%, 26 wywołań) implementują zmienne warunkowe POSIX,
używane do bardziej złożonych scenariuszy synchronizacji niż proste muteksy.
\texttt{pthread\_cond\_timedwait} różni się od \texttt{pthread\_cond\_wait} możliwością określenia limitu czasu oczekiwania (timeout). Użycie wersji z
timeoutem (85 vs 26 wywołań) sugeruje, że Unity stosuje wzorzec okresowego sprawdzania warunków zamiast nieograniczonego oczekiwania, co zwiększa
responsywność systemu.
Utworzenie 81 wątków (\texttt{pthread\_create}) podczas testu potwierdza rozbudowaną architekturę wielowątkową. Przy założeniu, że część wątków to wątki
robocze systemu zadań, sugeruje to pulę kilkudziesięciu wątków aktywnie uczestniczących w renderowaniu i logice gry.
\begin{table}[H]
\centering
\caption{Wywołania systemowe silnika Unity -- operacje I/O}
\label{tab:unity-osrt-io}
\begin{tabular}{|l|r|r|r|}
\hline
\textbf{Funkcja} & \textbf{Wywołania} & \textbf{Całk. czas (ms)} & \textbf{Śr. ($\mu$s)} \\
\hline
\texttt{poll} & 349 & 314,33 & 900,66 \\
\texttt{ioctl} & 1\,907 & 284,18 & 149,02 \\
\texttt{openat64} & 22\,155 & 23,80 & 1,07 \\
\texttt{read} & 235 & 2,89 & 12,28 \\
\texttt{open64} & 553 & 1,43 & 2,59 \\
\texttt{fopen} & 548 & 0,88 & 1,60 \\
\texttt{writev} & 261 & 0,50 & 1,90 \\
\texttt{fread} & 317 & 0,49 & 1,55 \\
\hline
\end{tabular}
\end{table}
Tabela~\ref{tab:unity-osrt-io} przedstawia statystyki operacji I/O. Funkcja \texttt{poll} (349 wywołań, 314,33 ms) służy do multipleksowanego
oczekiwania na zdarzenia z wielu deskryptorów plików -- w kontekście gry prawdopodobnie dotyczy komunikacji z systemem okienkowym (X11/Wayland)
oraz urządzeniami wejścia.
Duża liczba wywołań \texttt{openat64} (22\,155) wskazuje na intensywne operacje na systemie plików, prawdopodobnie związane z wczytywaniem zasobów
gry (tekstur, modeli, shaderów) z dysku. Średni czas 1,07 $\mu$s potwierdza efektywne buforowanie przez system operacyjny.
\texttt{ioctl} (1\,907 wywołań) służy do kontroli urządzeń -- w kontekście grafiki Vulkan jest
używane do komunikacji ze sterownikiem GPU poprzez
interfejs DRM/KMS (Direct Rendering Manager / Kernel Mode Setting).
Przeprowadzona analiza pozwala na sformułowanie następujących wniosków dotyczących wydajności i architektury silnika Unity:
Dominacja \texttt{vkWaitForFences} (95,2\% czasu Vulkan) i \texttt{futex} (95,9\% czasu systemowego)
jednoznacznie wskazuje na scenariusz
\textbf{GPU-bound}. Procesor główny efektywnie przygotowuje i przesyła pracę renderowania, po czym oczekuje na GPU. Jest to optymalny wzorzec dla
aplikacji graficznych, gdzie GPU wykonuje większość obliczeniowo intensywnej pracy.
W scenariuszu CPU-bound obserwowalibyśmy niższy udział funkcji
synchronizacyjnych i wyższy udział
funkcji przygotowujących polecenia
(\texttt{vkBeginCommandBuffer}, \\ \texttt{vkCmdBindPipeline} itp.), co wskazywałoby na wąskie gardło po stronie procesora.
Stosunek liczby wywołań \texttt{vkQueueSubmit} (27\,112) do \texttt{vkQueuePresentKHR} (13\,556) wynoszący 2:1 wskazuje na dwuetapowy potok
renderowania dla każdej klatki. Może to odpowiadać architekturze z oddzielnymi przebiegami dla sceny 3D i interfejsu użytkownika, lub użyciu techniki
odroczonego renderowania (ang. \textit{deferred rendering}).
Niska liczba wywołań \texttt{vkCmdBindPipeline} (27\,027, ~2 na klatkę)
sugeruje efektywne grupowanie obiektów renderowanych tym samym shaderem,
minimalizujące kosztowne zmiany stanu GPU.
Pomimo wysokiego współczynnika zmienności (153\%) wynikającego z wartości ekstremalnych podczas
inicjalizacji, właściwa stabilność renderowania jest
wysoka. Świadczy o tym:
\begin{itemize}
\item Wąski rozstęp międzykwartylowy (0,08 ms).
\item Zbieżność mediany (6,94 ms) ze średnią (6,95 ms).
\item Mała różnica między 50. a 99. percentylem (0,64 ms, 9,2\%).
\item 98,24\% klatek w przedziale 5--10 ms.
\end{itemize}
Należy jednak podkreślić, że obserwowana stabilność jest w znacznej mierze wynikiem
działania synchronizacji pionowej (V-Sync), która sztucznie
wyrównuje czasy klatek poprzez oczekiwanie na sygnał odświeżania monitora. Bez V-Sync
zmienność czasów klatek mogłaby być wyższa.
Analiza wywołań systemowych potwierdza intensywne wykorzystanie wielowątkowości:
\begin{itemize}
\item 81 utworzonych wątków wskazuje na rozbudowany system zadań.
\item Dominacja \texttt{futex} sugeruje częstą komunikację między wątkami.
\item Użycie zmiennych warunkowych z timeoutem świadczy o responsywnej architekturze.
\end{itemize}
Unity 2023 LTS stosuje architekturę DOTS (Data-Oriented Technology Stack) z systemem zadań (Job System), który automatycznie dystrybuuje pracę na
dostępne rdzenie procesora. Wyniki profilowania potwierdzają aktywne wykorzystanie tej architektury.
\subsection{Wyniki testów dla silnika Unreal Engine}
\label{subsec:wyniki-unreal}
Profilowanie silnika Unreal Engine 5.5 przeprowadzono przy użyciu NVIDIA Nsight Systems w
wersji 2025.5.2. Ze względu na problemy ze stabilnością
połączenia agenta Nsight podczas długich sesji profilowania, 90-sekundową rozgrywkę
podzielono na \textbf{trzy fazy po 30 sekund każda}:
\begin{itemize}
\item \textbf{Faza 1} (0--30 s): Początkowa rozgrywka z niską trudnością.
\item \textbf{Faza 2} (30--60 s): Środkowa rozgrywka ze średnią trudnością.
\item \textbf{Faza 3} (60--90 s): Końcowa rozgrywka z wysoką trudnością + ekran zwycięstwa.
\end{itemize}
Każda faza była uruchamiana z flagą \texttt{--start-time=N}, która przesuwa
zarówno stan gry
(w \texttt{STGGameDirector}), jak i poziom trudności
spawnu przeciwników \\ (w \texttt{STGEnemySpawner}) do odpowiedniej sekundy. Grę skompilowano w konfiguracji DebugGame, która zachowuje symbole
debugowania przy częściowych optymalizacjach.
Ze względu na bardzo dużą ilość danych generowanych przez Unreal Engine podczas śledzenia wywołań Vulkan API (około 13 milionów zdarzeń
na 30 sekund rozgrywki, w porównaniu z 0,5 miliona dla Unity), 90-sekundową rozgrywkę podzielono na \textbf{trzy fazy po 30 sekund każda}:
\begin{itemize}
\item \textbf{Faza 1} (0--30 s): Początkowa rozgrywka z niską trudnością.
\item \textbf{Faza 2} (30--60 s): Środkowa rozgrywka ze średnią trudnością.
\item \textbf{Faza 3} (60--90 s): Końcowa rozgrywka z wysoką trudnością + ekran zwycięstwa.
\end{itemize}
Każda faza była uruchamiana z flagą \texttt{--start-time=N},
która przesuwa zarówno stan gry
(w \texttt{STGGameDirector}), jak i poziom trudności spawnu przeciwników
\\ (w \texttt{STGEnemySpawner})
do odpowiedniej sekundy.
Grę skompilowano w konfiguracji DebugGame, która zachowuje symbole debugowania przy częściowych optymalizacjach.
Profilowanie przeprowadzono z wykorzystaniem tych samych metryk co dla Unity:
\begin{itemize}
\item \textbf{Śledzenia wywołań Vulkan API} (\texttt{--trace=vulkan}) -- przechwytywanie wszystkich funkcji Vulkan.
\item \textbf{Śledzenia wywołań systemowych} (\texttt{--trace=osrt}) -- przechwytywanie funkcji OS Runtime.
\item \textbf{Metryk sprzętowych GPU} (\texttt{--gpu-metrics-devices=0}) -- próbkowanie liczników wydajności GPU.
\end{itemize}
NVIDIA Nsight Systems zbiera metryki sprzętowe GPU poprzez bezpośredni dostęp do liczników
wydajności zintegrowanych w karcie graficznej.
Podczas trzech 35-sekundowych sesji (30 sekund rozgrywki + 5 sekund buforu) zebrano łącznie \textbf{1\,050\,555 próbek} dla każdej z
31 monitorowanych metryk.
\begin{table}[H]
\centering
\caption{Kluczowe metryki wykorzystania GPU dla silnika Unreal Engine (fazy 1--2, aktywna rozgrywka)}
\label{tab:unreal-gpu-metrics}
\begin{tabular}{|l|r|r|r|}
\hline
\textbf{Metryka} & \textbf{Średnia} & \textbf{Min.} & \textbf{Maks.} \\
\hline
GPU Active [\%] & 90,98 & 0 & 100 \\
GR Active [\%] & 85,59 & 0 & 100 \\
SMs Active [\%] & 42,88 & 0 & 100 \\
Sync Compute in Flight [\%] & 43,23 & 0 & 100 \\
Async Compute in Flight [\%] & 0,17 & 0 & 35 \\
SM Issue [\%] & 13,94 & 0 & 99 \\
\hline
\end{tabular}
\end{table}
Metryka \texttt{GPU Active} określa procentowy udział czasu, w którym karta graficzna wykonuje jakąkolwiek pracę obliczeniową.
Średnia wartość \textbf{90,98\%} dla faz 1--2 (aktywna rozgrywka) oznacza, że GPU był niemal w pełni wykorzystany podczas właściwej rozgrywki.
Faza 3 wykazała niższą wartość (49,55\%) ze względu na włączenie ekranu zwycięstwa i procesu zamykania gry.
\begin{table}[H]
\centering
\caption{Porównanie metryk GPU między fazami testu Unreal Engine}
\label{tab:unreal-gpu-phases}
\begin{tabular}{|l|r|r|r|}
\hline
\textbf{Metryka} & \textbf{Faza 1} & \textbf{Faza 2} & \textbf{Faza 3} \\
\hline
GPU Active [\%] & 91,16 & 90,80 & 49,55 \\
GR Active [\%] & 85,69 & 85,48 & 44,72 \\
SMs Active [\%] & 42,79 & 42,97 & 23,22 \\
Compute Warps [\%] & 13,05 & 13,00 & 7,03 \\
Pixel Warps [\%] & 9,45 & 9,26 & 4,68 \\
DRAM Read [\%] & 10,40 & 10,19 & 8,04 \\
DRAM Write [\%] & 10,19 & 10,00 & 5,60 \\
Liczba próbek & 350\,205 & 350\,249 & 350\,101 \\
\hline
\end{tabular}
\end{table}
Tabela~\ref{tab:unreal-gpu-phases} pokazuje stabilność metryk GPU między fazami 1 i 2
różnice <0,5 pp.),
co potwierdza poprawność metodologii
fazowego profilowania.
Wyraźny spadek w fazie
3 odzwierciedla zakończenie aktywnej rozgrywki i przejście do ekranu zwycięstwa.
Metryka \texttt{GR Active} (Graphics Active) mierzy wykorzystanie silnika graficznego (ang. \textit{graphics engine}) karty NVIDIA, odpowiedzialnego
za wykonywanie potoków renderowania (vertex, tessellation, geometry, fragment shaders). Średnia wartość \textbf{85,59\%} dla aktywnej rozgrywki
stanowi 94\% wartości \texttt{GPU Active} (90,98\%), co oznacza, że praca graficzna dominuje nad obliczeniami ogólnego przeznaczenia (compute shaders).
Różnica około 5 punktów procentowych między \texttt{GPU Active} a \texttt{GR Active} odpowiada pracy wykonanej przez jednostki compute i operacje
kopiowania pamięci, w tym asynchroniczny transfer danych przez Async Copy Engine (aktywny w 24--25\% czasu w fazach 1--2).
\texttt{SMs Active} (Streaming Multiprocessors Active) na poziomie \textbf{42,88\%} wskazuje, że średnio mniej niż połowa dostępnych multiprocesorów
strumieniowych jest aktywna jednocześnie. Karta NVIDIA RTX 3090 posiada 82 jednostki SM, więc średnio około 35 z nich wykonywało pracę w danym momencie.
Wartość \texttt{Sync Compute in Flight} (43,23\%) wskazuje na znaczące wykorzystanie synchronicznych shaderów obliczeniowych,
prawdopodobnie do operacji post-processingu, culling GPU lub przygotowania danych renderowania.
\begin{table}[H]
\centering
\caption{Metryki przepustowości pamięci GPU dla silnika Unreal Engine (fazy 1--2)}
\label{tab:unreal-memory-metrics}
\begin{tabular}{|l|r|r|}
\hline
\textbf{Metryka} & \textbf{Średnia (\%)} & \textbf{Maks. (\%)} \\
\hline
DRAM Read Bandwidth & 10,30 & 68,0 \\
DRAM Write Bandwidth & 10,10 & 78,0 \\
PCIe RX Throughput & 1,50 & 96,0 \\
PCIe TX Throughput & 1,39 & 17,0 \\
\hline
\end{tabular}
\end{table}
Tabela~\ref{tab:unreal-memory-metrics} przedstawia metryki przepustowości pamięci. Średnie wykorzystanie przepustowości odczytu
DRAM (\textbf{10,30\%}) i zapisu (\textbf{10,10\%}) jest umiarkowane, wskazując że pamięć nie stanowi głównego wąskiego gardła.
Wartości maksymalne (68\% i 78\%) pokazują, że w momentach szczytowych obciążenia przepustowość pamięci jest intensywnie wykorzystywana.
Stosunek odczytu do zapisu (10,30:10,10 $\approx$ 1,02:1) jest zbliżony do jedności, co sugeruje zbalansowany przepływ danych --
typowy dla nowoczesnych technik renderowania z wieloma przejściami i render targets.
\begin{table}[H]
\centering
\caption{Wykorzystanie różnych typów wątków shader GPU w silniku Unreal Engine (fazy 1--2)}
\label{tab:unreal-warps}
\begin{tabular}{|l|r|r|}
\hline
\textbf{Typ wątków (warps)} & \textbf{Średnia (\%)} & \textbf{Maks. (\%)} \\
\hline
Compute Warps in Flight & 13,03 & 93,0 \\
Pixel Warps in Flight & 9,36 & 99,0 \\
Vertex/Tess/Geometry Warps & 0,45 & 10,0 \\
Unallocated Warps in Active SMs & 20,73 & 90,0 \\
\hline
\end{tabular}
\end{table}
Tabela~\ref{tab:unreal-warps} przedstawia rozkład typów aktywnych wątków shader
(warps -- grupy 32 wątków CUDA wykonywanych synchronicznie).
Dominacja \texttt{Compute Warps} (13,03\%) nad \texttt{Pixel Warps} (9,36\%) wskazuje na znaczące wykorzystanie compute shaderów, prawdopodobnie do:
\begin{itemize}
\item Culling (odrzucanie niewidocznych obiektów na GPU).
\item Post-processing i tone mapping.
\item Symulacji cząsteczek lub fizyki na GPU.
\end{itemize}
Niski udział \texttt{Vertex/Tess/Geometry Warps} (0,45\%) sugeruje prostą geometrię sceny bez intensywnego wykorzystania teselacji --
co jest zgodne z charakterystyką testowanej gry bullet-hell, gdzie większość efektów wizualnych to płaskie sprite'y i efekty cząsteczkowe.
\texttt{Unallocated Warps in Active SMs} (20,73\%) reprezentuje niewykorzystaną pojemność
aktywnych multiprocesorów. Wartość ta wskazuje na
potencjał optymalizacji przez zwiększenie granularności pracy lub lepsze grupowanie operacji.
\begin{table}[H]
\centering
\caption{Częstotliwości zegara GPU podczas testu Unreal Engine}
\label{tab:unreal-gpu-clocks}
\begin{tabular}{|l|r|r|r|}
\hline
\textbf{Zegar} & \textbf{Średnia (MHz)} & \textbf{Min. (MHz)} & \textbf{Maks. (MHz)} \\
\hline
GPC Clock (Graphics) & 1\,887 & 1\,288 & 1\,965 \\
SYS Clock (Memory) & 1\,596 & 1\,080 & 1\,665 \\
\hline
\end{tabular}
\end{table}
Częstotliwości zegara (tabela~\ref{tab:unreal-gpu-clocks}) pokazują, że GPU działał ze średnią częstotliwością 1\,887 MHz
(96\% maksymalnej 1\,965 MHz), co wskazuje na niskie obciążenie termiczne pozwalające na utrzymanie wysokich częstotliwości boost bez throttlingu.
Minimalne wartości odpowiadają krótkim momentom niższego obciążenia podczas przejść między klatkami.
Dzięki zastosowaniu profilowania fazowego uzyskano \textbf{kompletne dane} śledzenia Vulkan API z całego 90-sekundowego przebiegu gry Unreal Engine.
Dane podzielone na trzy fazy (0--30s, 30--60s, 60--90s) umożliwiają szczegółową analizę ewolucji wykorzystania GPU w czasie rozgrywki.
\begingroup
\setlength{\textfloatsep}{0.5\baselineskip}
\setlength{\intextsep}{0.5\baselineskip}
\begin{table}[H]
\centering
\caption{Porównanie wywołań Vulkan API silnika Unreal Engine między fazami}
\label{tab:unreal-vulkan-phases}
\small
\begin{tabular}{|l|r|r|r|}
\hline
\textbf{Metryka} & \textbf{Faza 1 (0--30s)} & \textbf{Faza 2 (30--60s)} & \textbf{Faza 3 (60--90s)} \\
\hline
Liczba klatek (vkQueuePresentKHR) & 9\,964 & 10\,165 & 4\,846 \\
Średni FPS & 332 & 339 & 162 \\
vkCreateComputePipelines & 231 & 233 & 231 \\
vkCreateGraphicsPipelines & 793 & 797 & 816 \\
vkQueueSubmit & 166\,918 & 186\,589 & 74\,393 \\
Submit/klatkę & 16,2 & 16,2 & 16,2 \\
vkCmdBindPipeline & 2\,236\,013 & 2\,528\,014 & 1\,007\,615 \\
\hline
\end{tabular}
\end{table}
\endgroup
Tabela~\ref{tab:unreal-vulkan-phases} ujawnia znaczącą dynamikę wydajności między fazami.
Fazy 1 i 2 (aktywna rozgrywka) osiągają wysoką wydajność
(332--339 FPS), natomiast faza 3 pokazuje \textbf{znaczący spadek do 162 FPS} -- redukcję o
ponad 50\%. Spadek ten występuje w końcowej fazie
rozgrywki, gdy na ekranie znajduje się największa liczba przeciwników i pocisków, co stanowi
najbardziej wymagający moment dla silnika renderującego.
Dodatkowo faza 3 zawiera ekran zwycięstwa, który również wpływa na średnią wydajność.
\textbf{Uwaga metodologiczna:} Wartość 162 FPS z fazy 3 lepiej reprezentuje wydajność w wymagających
scenach niż średnie z faz 1--2, ponieważ faza 3 zawiera moment największego obciążenia gry
(maksymalna liczba przeciwników i pocisków na ekranie).
Stosunek wywołań \texttt{vkQueueSubmit} do \texttt{vkQueuePresentKHR} pozostaje stabilny na poziomie \textbf{16,2:1} we wszystkich fazach, co
wskazuje na konsystentną architekturę potoku renderowania niezależną od obciążenia sceny.
\begin{table}[H]
\centering
\caption{Wywołania Vulkan API silnika Unreal Engine -- tworzenie potoków (wszystkie fazy)}
\label{tab:unreal-vulkan-pipelines}
\small
\begin{tabular}{|l|r|r|r|r|}
\hline
\textbf{Funkcja} & \textbf{Czas (\%)} & \textbf{Wywołania} & \textbf{Śr. (ms)} & \textbf{Maks. (ms)} \\
\hline
\multicolumn{5}{|c|}{\textit{Faza 1 (0--30s)}} \\
\hline
\texttt{vkCreateComputePipelines} & 47,3 & 231 & 18,63 & 50,40 \\
\texttt{vkCreateGraphicsPipelines} & 10,0 & 793 & 1,14 & 36,68 \\
\texttt{vkCreateDevice} & 6,5 & 1 & 590,50 & 590,50 \\
\hline
\multicolumn{5}{|c|}{\textit{Faza 2 (30--60s)}} \\
\hline
\texttt{vkCreateComputePipelines} & 47,1 & 233 & 18,92 & 56,01 \\
\texttt{vkCreateGraphicsPipelines} & 11,2 & 797 & 1,31 & 36,43 \\
\texttt{vkCreateDevice} & 5,8 & 1 & 541,36 & 541,36 \\
\hline
\multicolumn{5}{|c|}{\textit{Faza 3 (60--90s)}} \\
\hline
\texttt{vkCreateComputePipelines} & 57,4 & 231 & 19,21 & 51,95 \\
\texttt{vkCreateGraphicsPipelines} & 14,6 & 816 & 1,39 & 40,88 \\
\texttt{vkCreateDevice} & 7,4 & 1 & 572,38 & 572,38 \\
\hline
\end{tabular}
\end{table}
W przeciwieństwie do Unity, gdzie dominującą funkcją był \texttt{vkWaitForFences},
w Unreal Engine \textbf{57--72\% czasu} Vulkan API
pochłonęły funkcje tworzenia potoków.
Co istotne, liczba wywołań \texttt{vkCreateComputePipelines} i
\texttt{vkCreateGraphicsPipelines} jest
\textbf{niemal identyczna we wszystkich trzech fazach}, co wskazuje na strategię \textbf{ciągłej rekompilacji potoków} (Pipeline State Object)
przez cały czas działania gry.
Łącznie w każdej 30-sekundowej fazie tworzonych jest około \textbf{1\,024--1\,047 potoków} (231 compute + 793--816 graphics). Porównując z Unity
(który utworzył tylko 3 potoki graficzne w całym 95-sekundowym teście), Unreal Engine generuje \textbf{ponad 300 razy więcej potoków}.
Średni czas tworzenia potoku compute (18,63--19,21 ms) jest ponad \textbf{14 razy dłuższy}
niż dla potoku graficznego (1,14--1,39 ms). Różnica ta
wynika z większej złożoności shaderów obliczeniowych używanych przez Unreal Engine do culling, post-processingu i systemu Nanite.
Wywołanie \texttt{vkCreateDevice} pojawia się raz w każdej fazie z czasem 541--590 ms, co odpowiada momentowi startu gry w tej fazie -- narzędzie
Nsight Systems tworzy nową sesję dla każdej fazy.
\begin{table}[H]
\centering
\caption{Wywołania Vulkan API silnika Unreal Engine -- synchronizacja i prezentacja (faza 2)}
\label{tab:unreal-vulkan-sync}
\small
\begin{tabular}{|l|r|r|r|r|}
\hline
\textbf{Funkcja} & \textbf{Czas (\%)} & \textbf{Wywołania} & \textbf{Śr. ($\mu$s)} & \textbf{Maks. (ms)} \\
\hline
\texttt{vkQueuePresentKHR} & 9,5 & 11\,531 & 77,05 & 0,90 \\
\texttt{vkQueueSubmit} & 7,8 & 186\,589 & 3,92 & 1,64 \\
\texttt{vkWaitForFences} & 0,5 & 11\,627 & 3,63 & 2,61 \\
\texttt{vkAcquireNextImageKHR} & 0,1 & 11\,531 & 0,89 & 7,55 \\
\hline
\end{tabular}
\end{table}
W ostrzym kontraście z Unity (gdzie \texttt{vkWaitForFences} stanowił 95,2\% czasu), w Unreal Engine funkcja ta pochłonęła zaledwie
\textbf{0,5\% czasu} ze średnim czasem oczekiwania 3,63 $\mu$s. Tak niski czas oczekiwania wskazuje na:
\begin{itemize}
\item Efektywne wykorzystanie wielokrotnego buforowania (triple buffering).
\item Asynchroniczne przesyłanie pracy do GPU bez blokowania.
\item Lepsze rozłożenie pracy między CPU a GPU eliminujące przestoje.
\end{itemize}
Stosunek wywołań \texttt{vkQueueSubmit} (186\,589) do \texttt{vkQueuePresentKHR} (11\,531)
wynosi \textbf{16,2:1}, co oznacza średnio 16
przesyłek pracy na klatkę. Jest to znacznie więcej niż w Unity (2:1), odzwierciedlając
bardziej złożony potok renderowania Unreal Engine z
wieloma przebiegami (deferred rendering, post-processing, UI).
\begin{table}[H]
\centering
\caption{Wywołania Vulkan API silnika Unreal Engine -- bufory poleceń (wszystkie fazy łącznie)}
\label{tab:unreal-vulkan-cmd}
\small
\begin{tabular}{|l|r|r|r|}
\hline
\textbf{Funkcja} & \textbf{Wywołania} & \textbf{Śr. ($\mu$s)} & \textbf{Maks. ($\mu$s)} \\
\hline
\texttt{vkCmdBindPipeline} & 5\,771\,642 & 0,24 & 2\,722 \\
\texttt{vkCmdPipelineBarrier2KHR} & 4\,090\,071 & 0,28 & 942 \\
\texttt{vkBeginCommandBuffer} & 427\,903 & 1,15 & 902 \\
\texttt{vkEndCommandBuffer} & 427\,900 & 0,78 & 228 \\
\hline
\end{tabular}
\end{table}
Liczba wywołań \texttt{vkCmdBindPipeline} (\textbf{5\,771\,642}
łącznie we wszystkich fazach) jest ponad \textbf{213 razy większa} niż w
Unity (27\,027), co odpowiada około 218 zmianom potoku na klatkę. Tak wysoka wartość wynika z:
\begin{itemize}
\item Dynamicznego systemu materiałów Unreal Engine.
\item Wielu wariantów shaderów dla różnych kombinacji oświetlenia.
\item Złożonego potoku renderowania z wieloma przebiegami.
\end{itemize}
Funkcja \texttt{vkCmdPipelineBarrier2KHR} (4\,090\,071 wywołań) synchronizuje dostęp do zasobów w obrębie GPU -- wysoka liczba wywołań wskazuje
na staranną kontrolę zależności między operacjami, typową dla nowoczesnych technik renderowania wykorzystujących wiele render targets.
Interesującą obserwacją jest obecność wywołań związanych z ray tracingiem we wszystkich fazach:
\begin{itemize}
\item \texttt{vkCreateAccelerationStructureKHR}: 23\,960 + 26\,275 + 11\,884 = 62\,119 wywołań.
\item \texttt{vkDestroyAccelerationStructureKHR}: 20\,571 + 23\,063 + 9\,181 = 52\,815 wywołań.
\item \texttt{vkGetAccelerationStructureBuildSizesKHR}: 41\,161 + 46\,147 + 18\,379 = 105\,687 wywołań.
\end{itemize}
Pomimo że testowana gra nie wykorzystuje widocznych efektów ray tracingu, Unreal Engine przygotowuje struktury akceleracji BVH
(Bounding Volume Hierarchy), prawdopodobnie do potencjalnego użycia w globalnym oświetleniu lub śledzeniu promieni. Nierówna liczba utworzeń
i zniszczeń sugeruje akumulację struktur w pamięci GPU podczas rozgrywki.
Podobnie jak dla Unity, Nsight Systems przechwycił wywołania funkcji systemowych we wszystkich trzech fazach, umożliwiając analizę zachowania
wielowątkowego Unreal Engine. Łącznie zarejestrowano ponad \textbf{9 milionów wywołań} funkcji synchronizacji.
\begin{table}[H]
\centering
\caption{Wywołania systemowe silnika Unreal Engine -- synchronizacja wątków (wszystkie fazy)}
\label{tab:unreal-osrt-sync}
\small
\begin{tabular}{|l|r|r|r|r|}
\hline
\textbf{Funkcja} & \textbf{Czas (\%)} & \textbf{Wywołania} & \textbf{Śr. (ms)} & \textbf{Maks. (s)} \\
\hline
\texttt{pthread\_cond\_wait} & 64,6 & 3\,095\,188 & 0,97 & 22,23 \\
\texttt{pthread\_cond\_timedwait} & 19,2 & 163\,783 & 5,46 & 2,00 \\
\texttt{poll} & 7,2 & 215\,851 & 1,56 & 0,10 \\
\texttt{usleep} & 4,7 & 26\,062 & 7,79 & 0,01 \\
\texttt{select} & 2,4 & 1\,039 & 99,72 & 0,10 \\
\texttt{nanosleep} & 0,6 & 755 & 35,55 & 0,20 \\
\hline
\end{tabular}
\end{table}
Funkcja \texttt{pthread\_cond\_wait} pochłonęła \textbf{64,6\% czasu} przy \textbf{3\,095\,188 wywołaniach} we wszystkich trzech fazach.
Jest to funkcja POSIX do oczekiwania na zmienną warunkową, używana gdy wątek musi czekać na spełnienie określonego warunku sygnalizowanego przez
inny wątek.
Tak wysoka liczba wywołań (ponad 40 razy więcej niż dla Unity)
odzwierciedla architekturę wielowątkową Unreal Engine opartą na systemie
\textbf{TaskGraph}. System ten dekomponuje pracę renderowania na małe zadania (ang. \textit{tasks}),
które są wykonywane przez pulę wątków roboczych.
Każde zadanie po zakończeniu sygnalizuje swoją gotowość, a zależne zadania są budzone poprzez
\texttt{pthread\_cond\_signal}/\texttt{pthread\_cond\_broadcast}.
Średni czas pojedynczego oczekiwania (0,97 ms) jest krótki, co wskazuje na częste, ale krótkotrwałe synchronizacje --
typowe dla drobnoziarnistego paralelizmu. Maksymalny czas 22,23 sekundy odpowiada prawdopodobnie wywołaniu podczas długotrwałej operacji
inicjalizacyjnej w fazie 2.
\begin{table}[H]
\centering
\caption{Porównanie wywołań synchronizacyjnych między fazami Unreal Engine}
\label{tab:unreal-osrt-phases}
\small
\begin{tabular}{|l|r|r|r|}
\hline
\textbf{Metryka} & \textbf{Faza 1} & \textbf{Faza 2} & \textbf{Faza 3} \\
\hline
\texttt{pthread\_cond\_wait} wywołań & 1\,166\,913 & 1\,253\,746 & 674\,529 \\
\texttt{pthread\_cond\_wait} czas (\%) & 63,2 & 65,1 & 66,4 \\
\texttt{pthread\_cond\_timedwait} wywołań & 68\,267 & 63\,863 & 31\,653 \\
\texttt{pthread\_cond\_broadcast} wywołań & 668\,650 & 747\,301 & 337\,258 \\
\texttt{backtrace} wywołań & 2\,306\,885 & 2\,289\,546 & 988\,685 \\
\hline
\end{tabular}
\end{table}
Tabela~\ref{tab:unreal-osrt-phases} pokazuje konsystencję wzorców wywołań między fazami 1 i 2 (aktywna rozgrywka) oraz wyraźny spadek w
fazie 3 (zawierającej ekran zwycięstwa). Szczególnie interesująca jest wysoka liczba wywołań \texttt{backtrace} (ponad 5,5 miliona łącznie),
co sugeruje intensywne wykorzystanie mechanizmów debugowania lub profilowania wbudowanych w Unreal Engine nawet w konfiguracji DebugGame.
(19,2\%, 163\,783 wywołań) różni się od
\texttt{pthread\_cond\_wait} możliwością określenia maksymalnego czasu
oczekiwania. Użycie tej funkcji wskazuje na mechanizmy:
\begin{itemize}
\item Timeoutów zapobiegających zakleszczeniom (deadlock prevention).
\item Okresowego sprawdzania warunków (polling pattern).
\item Synchronizacji czasowej dla frame pacing.
\end{itemize}
Średni czas 5,46 ms sugeruje użycie do synchronizacji między-klatkowej,
gdzie wątki oczekują na gotowość kolejnej klatki z timeout'em
zapobiegającym nieskończonemu oczekiwaniu w przypadku błędu.
Funkcja \texttt{usleep} (4,7\%, 26\,062 wywołań, średnio 7,79 ms) wprowadza precyzyjne opóźnienia czasowe. Średni czas 7,79 ms jest zbliżony do
czasu klatki przy ~128 FPS, co może sugerować mechanizm regulacji tempa renderowania lub oszczędzanie energii poprzez redukcję spin-waitingu.
\begin{table}[H]
\centering
\caption{Porównanie mechanizmów synchronizacji Unity i Unreal Engine (zaktualizowane)}
\label{tab:sync-comparison}
\small
\begin{tabular}{|l|r|r|}
\hline
\textbf{Metryka} & \textbf{Unity} & \textbf{Unreal Engine} \\
\hline
Dominujący mechanizm & \texttt{futex} (95,9\%) & \texttt{pthread\_cond\_wait} (64,6\%) \\
Liczba wywołań synchronizacji & 247 & 3\,095\,188 \\
Średni czas wywołania & 444,07 ms & 0,97 ms \\
Utworzone wątki & 81 & $\sim$83 \\
Liczba próbek GPU (10 kHz) & -- & 1\,050\,555 \\
\hline
\end{tabular}
\end{table}
Tabela~\ref{tab:sync-comparison} ujawnia fundamentalną różnicę architektoniczną między silnikami:
\textbf{Unity} stosuje mechanizm \texttt{futex} z niewielką liczbą wywołań (247) i
długim średnim czasem (444 ms). Wskazuje to na architekturę z
większymi, bardziej autonomicznymi jednostkami pracy i rzadszą synchronizacją między wątkami.
\textbf{Unreal Engine} używa \texttt{pthread\_cond\_wait} z ogromną liczbą wywołań
(ponad 3 miliony w 90-sekundowym teście) i bardzo krótkim średnim
czasem (0,97 ms). Odzwierciedla to drobnoziarnisty paralelizm systemu TaskGraph, gdzie praca jest dzielona na małe zadania często komunikujące się ze
sobą.
Różnica ta ma implikacje praktyczne:
\begin{itemize}
\item \textbf{Skalowalność}: Drobnoziarnisty model Unreal lepiej skaluje się na procesory z wieloma rdzeniami.
\item \textbf{Narzut synchronizacji}: Model Unity ma mniejszy narzut z powodu rzadszych wywołań.
\item \textbf{Responsywność}: Unreal może szybciej reagować na zmiany (np. przerwanie zadania).
\item \textbf{Debugowanie}: Model Unity jest łatwiejszy do analizy ze względu na prostszą strukturę.
\end{itemize}
Zebrane dane z trzech faz profilowania pozwalają na charakterystykę architektonicznych aspektów silnika Unreal Engine:
Unreal Engine 5 stosuje zaawansowaną architekturę wielowątkową złożoną z:
\begin{itemize}
\item \textbf{Game Thread} -- główny wątek logiki gry.
\item \textbf{Render Thread} -- wątek przygotowujący polecenia renderowania.
\item \textbf{RHI Thread} (Render Hardware Interface) -- wątek komunikujący się z API graficznym.
\item \textbf{Worker Threads} -- pula wątków roboczych systemu TaskGraph.
\end{itemize}
Obserwowana dominacja \texttt{pthread\_cond\_wait} (3+ miliony wywołań) potwierdza intensywną
komunikację między tymi wątkami.
Wysokie wykorzystanie
GPU (90,98\% w fazach aktywnej rozgrywki) przy jednoczesnej intensywnej synchronizacji CPU sugeruje efektywne wykorzystanie zasobów obu procesorów.
Na podstawie zebranych metryk można scharakteryzować profil obciążenia GPU:
\begin{itemize}
\item \textbf{Charakter pracy}: Mieszany graficzno-obliczeniowy (GR Active 85,59\%, Sync Compute 43,23\%).
\item \textbf{Wykorzystanie SM}: Umiarkowane (42,88\%), wskazujące na potencjał optymalizacji.
\item \textbf{Przepustowość pamięci}: Niewysoka (10,30\% odczyt, 10,10\% zapis), nie jest wąskim gardłem.
\item \textbf{Transfer PCIe}: Niski (1,50\% RX), dane pozostają w pamięci GPU.
\item \textbf{Async Copy Engine}: Aktywny w 24--25\% czasu, wskazując na efektywne wykorzystanie asynchronicznych transferów.
\end{itemize}
Porównanie faz 1 i 2 (tabela~\ref{tab:unreal-gpu-phases}) pokazuje niezwykłą stabilność metryk GPU:
\begin{itemize}
\item GPU Active: różnica 0,36 pp. (91,16\% vs 90,80\%).
\item GR Active: różnica 0,21 pp. (85,69\% vs 85,48\%).
\item SMs Active: różnica 0,18 pp. (42,79\% vs 42,97\%).
\end{itemize}
Ta konsystencja potwierdza poprawność metodologii fazowego profilowania i sugeruje deterministyczne zachowanie silnika renderującego niezależnie od
poziomu trudności gry.
Dzięki profilowaniu fazowemu uzyskano kompletne dane śledzenia Vulkan API i metryk GPU dla całej 90-sekundowej rozgrywki.
Zebrane dane (ponad 32 miliony zdarzeń Vulkan API, ponad milion próbek GPU i ponad 9 milionów wywołań systemowych) dostarczają
kompleksowego wglądu w charakterystykę wydajnościową silnika, umożliwiając bezpośrednie porównanie z Unity.
\subsection{Analiza porównawcza}
\label{subsec:analiza-porownawcza}
\begin{table}[H]
\centering
\caption{Porównanie czasów klatek i wydajności między silnikami}
\label{tab:frame-time-comparison}
\begin{tabular}{|l|r|r|}
\hline
\textbf{Metryka} & \textbf{Unity} & \textbf{Unreal Engine} \\
\hline
Średni FPS (fazy 1--2) & 164 (V-Sync) & 332--339 \\
Średni FPS (faza 3, wymagająca) & 164 (V-Sync) & 162 \\
Całkowita liczba klatek (90s) & 14\,765 & 24\,975 \\
\hline
\end{tabular}
\end{table}
Tabela~\ref{tab:frame-time-comparison} przedstawia porównanie wydajności obu silników.
Kluczową obserwacją jest to, że \textbf{Unity działał z włączonym V-Sync} na monitorze 165 Hz,
co ograniczało wydajność do około 164 FPS niezależnie od obciążenia sceny. Unreal Engine działał
bez V-Sync, osiągając 332--339 FPS w fazach 1--2, jednak \textbf{w fazie 3 (najbardziej wymagającej)
wydajność spadła do 162 FPS} -- wartości zbliżonej do Unity.
Ten wynik sugeruje, że przy wysokim obciążeniu sceny (maksymalna liczba przeciwników i pocisków)
oba silniki osiągają porównywalną wydajność, natomiast Unreal Engine jest w stanie wykorzystać
zapas mocy obliczeniowej GPU przy mniejszym obciążeniu.
\begin{table}[H]
\centering
\caption{Porównanie wykorzystania GPU między silnikami}
\label{tab:gpu-comparison}
\footnotesize
\begin{tabular}{|l|r|r|}
\hline
\textbf{Metryka} & \textbf{Unity} & \textbf{Unreal Engine} \\
\hline
Dominująca funkcja Vulkan & vkWaitForFences (95,2\%) & vkCreateComputePipelines (47--57\%) \\
Charakter ograniczenia & GPU-bound & Pipeline compilation \\
vkQueueSubmit / klatkę & 2 & 16,2 \\
vkCmdBindPipeline / klatkę & 2 & 218 \\
\hline
\end{tabular}
\end{table}
Analiza wywołań Vulkan API ujawnia fundamentalnie różne profile obciążenia (tabela~\ref{tab:gpu-comparison}):
Dominacja \texttt{vkWaitForFences} (95,2\% czasu) wskazuje, że CPU efektywnie
przygotowuje pracę i oczekuje na GPU.
Jest to pożądany wzorzec w aplikacjach graficznych, gdzie GPU wykonuje
większość obliczeń. Niski stosunek
\texttt{vkQueueSubmit}/klatkę (2:1) świadczy o prostym, dwuetapowym potoku
renderowania.
W Unreal Engine dominującymi operacjami były \texttt{vkCreateComputePipelines}
oraz \texttt{vkCreateGraphicsPipelines},
pochłaniające łącznie 57--72\% czasu Vulkan. Silnik tworzy około \textbf{1000 potoków w każdej 30-sekundowej fazie}
(vs 3 potoki w całym teście Unity), co wskazuje na strategię dynamicznej kompilacji shaderów.
Wysoki stosunek \texttt{vkCmdBindPipeline}/klatkę (218:1 vs 2:1) odzwierciedla złożony system materiałów Unreal
z wieloma wariantami shaderów, co wprowadza znaczący narzut zmian stanu GPU.
\begin{table}[H]
\centering
\caption{Porównanie mechanizmów synchronizacji między silnikami}
\label{tab:threading-comparison}
\small
\begin{tabular}{|l|r|r|}
\hline
\textbf{Metryka} & \textbf{Unity} & \textbf{Unreal Engine} \\
\hline
Główny mechanizm synchronizacji & futex & pthread\_cond\_wait \\
Liczba wywołań synchronizacji & 247 & 3\,095\,188 \\
Średni czas wywołania & 444 ms & 0,97 ms \\
Model paralelizmu & Gruboziarnisty & Drobnoziarnisty \\
\hline
\end{tabular}
\end{table}
Tabela~\ref{tab:threading-comparison} ujawnia fundamentalną różnicę architektoniczną:
\textbf{Unity} stosuje model gruboziarnistego paralelizmu z rzadkimi, ale długimi synchronizacjami. Wątki wykonują
większe jednostki pracy autonomicznie, co minimalizuje narzut komunikacji.
\textbf{Unreal Engine} implementuje drobnoziarnisty paralelizm poprzez system TaskGraph. Praca jest dzielona na tysiące
małych zadań często komunikujących się ze sobą (ponad 3 miliony wywołań synchronizacji w 90 sekund).
\subsection{Podsumowanie wyników testów wydajności}
\label{subsec:podsumowanie-testow}
\begin{table}[H]
\centering
\caption{Zestawienie kluczowych wyników testów wydajności}
\label{tab:wyniki-wydajnosci}
\begin{tabular}{|l|r|r|}
\hline
\textbf{Metryka} & \textbf{Unity} & \textbf{Unreal Engine} \\
\hline
Średni FPS (fazy 1--2) & 164 (V-Sync) & 332--339 \\
FPS w wymagającej scenie & 132 (1\% low) & 162 (faza 3) \\
GPU Active (\%) & 23 & 91 (fazy 1--2), 50 (faza 3) \\
Dominujące wąskie gardło & GPU (rendering) & CPU (kompilacja potoków) \\
Wywołania Vulkan API & $\sim$0,5 mln & $\sim$32 mln \\
Wywołania synchronizacji OS & 29\,383 & $\sim$9 mln \\
Potoki graficzne utworzone & 3 & $\sim$2\,400 \\
\hline
\end{tabular}
\end{table}
Przeprowadzone testy wydajnościowe pozwalają na sformułowanie następujących wniosków:
\begin{enumerate}
\item \textbf{Wydajność w wymagających scenach}: W fazie 3 (maksymalne obciążenie)
oba silniki osiągają zbliżoną wydajność: Unity 1\% low 132 FPS vs Unreal 162 FPS. Różnica
około 23\% na korzyść Unreal wynika częściowo z różnych konfiguracji V-Sync.
\item \textbf{Wykorzystanie GPU}: Unity wykorzystuje jedynie 23\% mocy GPU
\\ (ograniczony V-Sync),
podczas gdy Unreal Engine osiąga 91\% wykorzystania w fazach 1--2. Sugeruje to znaczny
zapas wydajności Unity przy wyłączonym V-Sync.
\item \textbf{Stabilność}: Unity wykazał stabilne czasy klatek dzięki
V-Sync, natomiast
Unreal Engine pokazał dużą zmienność między fazami (332--339 FPS w fazach 1--2 vs 162 FPS
w fazie 3) -- spadek o ponad 50\%.
\item \textbf{Architektura}: Silniki stosują fundamentalnie różne podejścia do
wielowątkowości i zarządzania potokami renderowania. Unity używa gruboziarnistego paralelizmu
z rzadkimi synchronizacjami, podczas gdy Unreal stosuje drobnoziarnisty system TaskGraph
z milionami wywołań synchronizacyjnych.
\item \textbf{Narzut Unreal}: Dynamiczna kompilacja potoków (ponad 1000 potoków na
\\ 30-sekundową fazę vs 3 w całym teście Unity) i 60-krotnie większa liczba wywołań Vulkan API
stanowią znaczący narzut, który może przyczyniać się do spadków wydajności w wymagających scenach.
\end{enumerate}