\clearpage \section{Testy wydajności} \label{sec:testy-wydajnosci} \subsection{Metryki wydajności} \subsubsection{Zbierane dane} 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. \subsubsection{Ogólne wyniki wydajności} 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). Jest to wynik znacząco przewyższający standardowy cel wydajnościowy 60 FPS dla aplikacji interaktywnych, wskazujący na bardzo dobrą optymalizację silnika Unity dla testowanej sceny. \begin{table}[htbp] \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) wymaga szczególnej uwagi. Tak długi czas klatki 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. \subsubsection{Analiza rozkładu czasów klatek} 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}[htbp] \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 ze średnią arytmetyczną (6,95 ms), co wskazuje na symetryczny rozkład czasów klatek w normalnych warunkach pracy. W praktyce oznacza to, że typowa klatka renderowana jest w czasie bardzo zbliżonym do wartości średniej. Szczególnie istotny jest \textbf{99. percentyl} wynoszący 7,58 ms. Wartość ta, określana w środowisku graczy jako ,,1\% low'', reprezentuje wydajność w najgorszych 1\% przypadków. Różnica między medianą (6,94 ms) a 99. percentylem (7,58 ms) wynosi zaledwie 0,64 ms (9,2\%), co świadczy o \textbf{wyjątkowej stabilności} renderowania. Dla porównania, w wielu grach różnica ta przekracza 50\%, co objawia się zauważalnymi ,,przycięciami'' obrazu. \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}[htbp] \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. \subsubsection{Szczegółowa analiza wywołań Vulkan API} 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}[htbp] \centering \caption{Wywołania Vulkan API silnika Unity -- funkcje synchronizacji i prezentacji} \label{tab:unity-vulkan-sync} \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} \paragraph{Funkcja vkWaitForFences -- synchronizacja CPU-GPU} Funkcja \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. \paragraph{Funkcja vkQueuePresentKHR -- prezentacja klatek} Funkcja \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). \paragraph{Funkcja vkQueueSubmit -- przesyłanie pracy do GPU} \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}[htbp] \centering \caption{Wywołania Vulkan API silnika Unity -- bufory poleceń} \label{tab:unity-vulkan-cmd} \begin{tabular}{|l|r|r|r|r|} \hline \textbf{Funkcja} & \textbf{Wywołania} & \textbf{Śr. (\textmu{}s)} & \textbf{Med. (\textmu{}s)} & \textbf{Maks. (\textmu{}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} \paragraph{Nagrywanie buforów poleceń} Tabela~\ref{tab:unity-vulkan-cmd} przedstawia statystyki funkcji związanych z buforami poleceń. Liczba wywołań \texttt{vkBeginCommandBuffer} i \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}[htbp] \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} \paragraph{Operacje inicjalizacyjne} 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. \subsubsection{Analiza wywołań systemowych (OS Runtime)} 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}[htbp] \centering \caption{Wywołania systemowe silnika Unity -- synchronizacja wątków} \label{tab:unity-osrt-sync} \begin{tabular}{|l|r|r|r|r|r|} \hline \textbf{Funkcja} & \textbf{Czas (\%)} & \textbf{Wywołania} & \textbf{Śr. (ms)} & \textbf{Med. (\textmu{}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} \paragraph{Mechanizm futex -- szybka synchronizacja w przestrzeni użytkownika} 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 \textmu{}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. \paragraph{Zmienne warunkowe POSIX} 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}[htbp] \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. (\textmu{}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} \paragraph{Operacje wejścia/wyjścia} 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 \textmu{}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). \subsubsection{Interpretacja wyników i wnioski} Przeprowadzona analiza pozwala na sformułowanie następujących wniosków dotyczących wydajności i architektury silnika Unity: \paragraph{Charakterystyka ograniczenia wydajności} 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. \paragraph{Efektywność potoku renderowania} 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. \paragraph{Stabilność czasów klatek} Pomimo wysokiego współczynnika zmienności (153\%) wynikającego z wartości ekstremalnych podczas inicjalizacji, właściwa stabilność renderowania jest \textbf{doskonała}. Ś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} Tak wysoka stabilność jest kluczowa dla komfortu gracza -- nawet wysoki FPS nie gwarantuje płynności, jeśli czasy klatek są zmienne. Unity osiąga tutaj wynik zbliżony do standardów wymaganych przez aplikacje VR. \paragraph{Architektura wielowątkowa} 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. \subsubsection{Ograniczenia metodologiczne profilowania Unreal Engine} Podczas prób profilowania silnika Unreal Engine 5.5 napotkano istotne ograniczenie techniczne: \textbf{śledzenie wywołań Vulkan API powoduje awarię} (ang. \textit{crash}) skompilowanej gry zarówno w konfiguracji Shipping, jak i DebugGame. Problem ten występuje przy uruchomieniu z parametrem \texttt{--trace=vulkan} narzędzia Nsight Systems i objawia się błędem segmentacji (\textit{segmentation fault}) krótko po starcie aplikacji. Przyczyna tego zachowania prawdopodobnie wynika z interakcji między mechanizmem instrumentacji Nsight a kodem Unreal Engine, która uniemożliwia bezpieczne przechwytywanie wywołań Vulkan. W związku z tym ograniczeniem, profilowanie Unreal Engine przeprowadzono z wykorzystaniem: \begin{itemize} \item \textbf{Metryk sprzętowych GPU} (\texttt{--gpu-metrics-devices=0}) -- bezpośrednie próbkowanie liczników wydajności karty graficznej NVIDIA z częstotliwością 10\,000 Hz \item \textbf{Śledzenia wywołań systemowych} (\texttt{--trace=osrt}) -- przechwytywanie funkcji OS Runtime (pthread, futex, poll itp.) \end{itemize} Konsekwencją tego ograniczenia jest \textbf{brak bezpośrednich danych o czasach klatek} i liczbie wyrenderowanych klatek dla Unreal Engine. Analiza porównawcza musi zatem opierać się na metrykach wykorzystania GPU i wzorcach wywołań systemowych, które dostarczają pośrednich informacji o wydajności renderowania. \subsubsection{Metryki wykorzystania GPU} 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}[htbp] \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} \paragraph{GPU Active -- ogólna aktywność karty graficznej} 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}[htbp] \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. \paragraph{GR Active -- aktywność silnika graficznego} 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). \paragraph{SMs Active i Sync Compute -- wykorzystanie multiprocesorów strumieniowych} \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}[htbp] \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} \paragraph{Przepustowość pamięci VRAM} 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}[htbp] \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} \paragraph{Analiza wątków shaderów (warps)} 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}[htbp] \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. \subsubsection{Analiza wywołań Vulkan API w trzech fazach} 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. \begin{table}[htbp] \centering \caption{Porównanie wywołań Vulkan API silnika Unreal Engine między fazami} \label{tab:unreal-vulkan-phases} \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) & 10\,286 & 11\,531 & 4\,590 \\ Średni FPS & 343 & 384 & 153 \\ 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} \paragraph{Dynamika wydajności między fazami} Tabela~\ref{tab:unreal-vulkan-phases} ujawnia interesującą dynamikę wydajności. Faza 2 (środkowa część rozgrywki) osiąga najwyższą wydajność ze średnio \textbf{384 FPS}, podczas gdy faza 3 (zawierająca ekran zwycięstwa) pokazuje znaczący spadek do \textbf{153 FPS}. Spadek ten związany jest z zakończeniem rozgrywki i przejściem do ekranu końcowego. 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}[htbp] \centering \caption{Wywołania Vulkan API silnika Unreal Engine -- tworzenie potoków (wszystkie fazy)} \label{tab:unreal-vulkan-pipelines} \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} \paragraph{Kompilacja potoków -- ciągły proces} 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}[htbp] \centering \caption{Wywołania Vulkan API silnika Unreal Engine -- synchronizacja i prezentacja (faza 2)} \label{tab:unreal-vulkan-sync} \begin{tabular}{|l|r|r|r|r|} \hline \textbf{Funkcja} & \textbf{Czas (\%)} & \textbf{Wywołania} & \textbf{Śr. (\textmu{}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} \paragraph{Synchronizacja -- minimalne oczekiwanie na GPU} 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 \textmu{}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}[htbp] \centering \caption{Wywołania Vulkan API silnika Unreal Engine -- bufory poleceń (wszystkie fazy łącznie)} \label{tab:unreal-vulkan-cmd} \begin{tabular}{|l|r|r|r|} \hline \textbf{Funkcja} & \textbf{Wywołania} & \textbf{Śr. (\textmu{}s)} & \textbf{Maks. (\textmu{}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} \paragraph{Bufory poleceń -- intensywna zmiana stanów} 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. \paragraph{Ray tracing -- przygotowanie struktur akceleracji} 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. \subsubsection{Analiza wywołań systemowych Unreal Engine} 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}[htbp] \centering \caption{Wywołania systemowe silnika Unreal Engine -- synchronizacja wątków (wszystkie fazy)} \label{tab:unreal-osrt-sync} \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} \paragraph{pthread\_cond\_wait -- architektura TaskGraph} 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}[htbp] \centering \caption{Porównanie wywołań synchronizacyjnych między fazami Unreal Engine} \label{tab:unreal-osrt-phases} \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. \paragraph{pthread\_cond\_timedwait -- synchronizacja z limitem czasowym} \texttt{pthread\_cond\_timedwait} (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. \paragraph{usleep -- precyzyjne opóźnienia} 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}[htbp] \centering \caption{Porównanie mechanizmów synchronizacji Unity i Unreal Engine (zaktualizowane)} \label{tab:sync-comparison} \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} \paragraph{Porównanie modeli synchronizacji} 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} \subsubsection{Charakterystyka architektury Unreal Engine na podstawie profilowania} Zebrane dane z trzech faz profilowania pozwalają na charakterystykę architektonicznych aspektów silnika Unreal Engine: \paragraph{Model wielowątkowości} 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. \paragraph{Profil obciążenia GPU} 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} \paragraph{Stabilność między fazami} 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. \paragraph{Ograniczenia analizy} Brak danych o wywołaniach Vulkan API (z powodu crashu przy włączonym \texttt{--trace=vulkan}) uniemożliwia: \begin{itemize} \item Bezpośrednie porównanie liczby klatek i FPS z Unity \item Analizę strategii synchronizacji fence/semaphore \item Identyfikację konkretnych operacji renderowania \item Ocenę efektywności batching'u i state changes \end{itemize} Niemniej zebrane metryki GPU (ponad milion próbek) i wywołania systemowe (ponad 9 milionów wywołań) dostarczają wartościowego wglądu w charakterystykę wydajnościową silnika, pozwalając na porównanie architektoniczne z Unity mimo braku bezpośrednich danych o frame time. \subsection{Analiza porównawcza} \label{subsec:analiza-porownawcza} \subsubsection{Porównanie czasu klatki} \begin{table}[htbp] \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 (aktywna rozgrywka) & 144 & 343--384 \\ Mediana czasu klatki (ms) & 6,94 & 2,60--2,91 \\ 99. percentyl czasu klatki (ms) & 7,58 & -- \\ Całkowita liczba klatek (90s) & 13\,556 & 26\,419 \\ \hline \end{tabular} \end{table} Tabela~\ref{tab:frame-time-comparison} przedstawia bezpośrednie porównanie wydajności obu silników. Unreal Engine osiągnął \textbf{ponad 2,5-krotnie wyższy FPS} niż Unity (343--384 vs 144 FPS) podczas aktywnej rozgrywki. Różnica ta jest zaskakująca, biorąc pod uwagę, że Unity jest powszechnie uważany za lepiej zoptymalizowany dla gier 2D. Możliwe przyczyny przewagi Unreal Engine: \begin{itemize} \item \textbf{Architektura renderowania} -- Unreal stosuje bardziej agresywne techniki optymalizacji (culling na GPU, batching dynamiczny), które są efektywne nawet dla prostych scen 2D \item \textbf{Kompilacja natywna} -- kod C++ Unreal jest kompilowany bezpośrednio do kodu maszynowego, eliminując narzut maszyny wirtualnej .NET/Mono \item \textbf{Wielowątkowość} -- drobnoziarnisty paralelizm TaskGraph pozwala na lepsze wykorzystanie wielordzeniowego procesora \end{itemize} Należy jednak zauważyć, że Unity wykazał \textbf{lepszą stabilność czasów klatek} -- 99. percentyl (7,58 ms) był zaledwie 9,2\% wyższy od mediany (6,94 ms), co świadczy o przewidywalnym zachowaniu potoku renderowania. \subsubsection{Porównanie wykorzystania GPU} \begin{table}[htbp] \centering \caption{Porównanie wykorzystania GPU między silnikami} \label{tab:gpu-comparison} \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}): \paragraph{Unity -- scenariusz GPU-bound} 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. \paragraph{Unreal Engine -- kompilacja potoków jako wąskie gardło} W Unreal Engine dominującymi operacjami były \texttt{vkCreateComputePipelines} i \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. \subsubsection{Porównanie architektury wielowątkowej} \begin{table}[htbp] \centering \caption{Porównanie mechanizmów synchronizacji między silnikami} \label{tab:threading-comparison} \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}[htbp] \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 & 144 & 343--384 \\ Mediana czasu klatki (ms) & 6,94 & 2,60--2,91 \\ GPU Active (\%) & -- & 90,98 \\ Dominujące wąskie gardło & GPU (rendering) & CPU (kompilacja potoków) \\ Wywołania Vulkan API & 218\,815 & $\sim$15 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{Surowa wydajność}: Unreal Engine osiągnął wyższą liczbę klatek na sekundę (343--384 vs 144 FPS), co przeczy hipotezie o przewadze Unity w grach 2D \item \textbf{Stabilność}: Unity wykazał lepszą stabilność czasów klatek z wąskim rozstępem międzykwartylowym (0,08 ms) i małą różnicą między medianą a 99. percentylem (9,2\%) \item \textbf{Architektura}: Silniki stosują fundamentalnie różne podejścia do wielowątkowości i zarządzania potokami renderowania, co ma implikacje dla skalowalności i responsywności \item \textbf{Narzut Unreal}: Dynamiczna kompilacja potoków i drobnoziarnisty paralelizm wprowadzają znaczący narzut (15 mln wywołań Vulkan vs 219 tys.), który jednak nie przekłada się na niższą wydajność końcową \end{enumerate}