Автор вспоминает опыт с ZX81/ZX Spectrum, где игры загружались с аудиокассет, и проводит параллель с современными, всё ещё не мгновенными загрузками игр, несмотря на SSD, быстрый CPU и VRAM.
Он решает профилировать загрузку крупного боя в Ridiculous Space Battles — космической тактической игре с большими сражениями. Профилирование через AMD UProf сначала показывает много времени в текстовом движке, но это рендеринг, не сама загрузка. Переход к flame graph позволяет выделить три ключевых этапа при загрузке миссии:
- загрузка кораблей,
- загрузка фона,
- preload ассетов через GUI_LoadingBar (список текстур, нужных в бою).
Главным подозреваемым становится DirectX 9-функция D3DXCreateTextureFromFileInMemoryEx, которая обрабатывает уже считанные в память файлы (чаще всего DDS). Автор заранее оптимизировал ввод-вывод:
- чтение файла целиком в буфер RAM до вызова DirectX,
- наличие собственного pak-формата (1,8 ГБ) для склейки ресурсов и уменьшения накладных расходов файловой системы и антивируса.
Переход на pak-файл почти не меняет время загрузки, что заставляет усомниться в профайлере. Вводятся собственные таймеры: общий вызов GUI_Game::Activate() занимает ~3831 мс, а инициализация фона — всего 0,0099 мс, что выглядит абсурдно. Переключение UProf на time-based sampling подтверждает, что PNG-обработка в D3DX тоже медленная, но часть фонов, вероятно, уже в кэше RAM.
Автор логирует время загрузки отдельных файлов:
composite3.png ~73 мс scanlines.bmp ~20 мс крупные DDS кораблей ~7–12 мсФоновые PNG (2048×2048) дают широкий разброс по времени (около 100–240 мс). Для проверки влияния кэша он:
- загружает Battlefield 2042, чтобы занять RAM,
- делает несколько прогонов,
- делает жёсткую перезагрузку.
После перезагрузки видно, что перевод больших фонов с PNG в DDS — неудачен: DDS-файл (16 МБ вместо 2 МБ PNG) иногда грузится быстрее, но в «чистом» тесте может быть даже медленнее (порядка 200 мс против ~100–140 мс). Вклад этих файлов в общий GUI_Game::Activate() — всего 1–2%, тогда как весь акт активации экрана боя занимает уже ~7847 мс.
Детальное логирование показывает, что собственный код до и после вызова D3DX занимает доли миллисекунды, а почти всё время уходит в D3DXCreateTextureFromFileInMemoryEx (70–110 мс на большой фон). Это подтверждает: узкое место — именно D3DX-путь обработки текстур, а не файловый ввод-вывод.
По совету Grok автор пишет собственную реализацию загрузчика DDS вместо D3DX. Однако повторные замеры показывают почти те же цифры: основной расход времени по-прежнему внутри обработки данных (включая memcpy), а не в его «обвязке». Переход к профилировщику Visual Studio выявляет, что значимую долю времени занимают:
- чтение файла с диска,
- многочисленные вызовы memcpy в собственном «быстром» DDS-лоадере.
Структура DDS-лоадера такова, что для большого текстурного файла (2048×2048) выполняется до 2048 вызовов memcpy — по строке на вызов. Это становится новым очевидным кандидатом на оптимизацию (слияние копирований в большие блоки, изменение формата/лейаута, возможно, другой путь загрузки в DirectX). На этом месте автор вынужден прервать работу.
Выводы
- Профилирование загрузки должно опираться на собственные таймеры и разные режимы профайлера — одних flame graph недостаточно.
- Основной bottleneck в данном кейсе — не файловый ввод-вывод, а CPU-обработка текстур в D3DX/собственном DDS-лоадере.
- Перевод больших фонов с PNG на DDS не гарантирует ускорения: размер на диске и поведение кэша могут нивелировать выигрыш.
- Многочисленные мелкие memcpy (по строкам текстуры) становятся заметным узким местом и требуют переработки алгоритма загрузки.
- Оптимизация загрузки — это серия проверок гипотез (формат, pak, собственный лоадер), многие из которых оказываются тупиковыми, но дают понимание реальных узких мест.