Коммит bfe12549 создал по автору Nikitos's avatar Nikitos
Просмотр файлов

Выбор GPU-бэкенда и понятный отказ D3D12 без DXIL

Пробел из docs/TASKS.md §12.5. Раньше бэкенд выбирала SDL, а при неудаче
печаталась одна строка «SDL_CreateGPUDevice: …» — без имени бэкенда и без
списка доступных, поэтому на машине с одним рабочим бэкендом приходилось
угадывать, а окно выглядело пустым.

* --gpu <имя> и переменная R2D_GPU выбирают бэкенд (metal, vulkan, direct3d12;
  привычное «d3d12» принимается); --list-gpu печатает собранные бэкенды и
  выходит, не открывая окно;
* при недоступном бэкенде отказ перечисляет собранные альтернативы, а не
  оставляет окно-зомби;
* для D3D12 добавлена прямая подсказка: он работает только с DXIL, нужна сборка
  с DXC либо другой бэкенд;
* имя бэкенда передаётся в r2d_app_init аргументом: там структура приложения
  обнуляется (SDL_zero), и записать поле до вызова было бы недостаточно — этот
  баг нашёлся при первой же проверке (запрос молча игнорировался).

Проверка: tests/agent/cli_gpu_test.py (5 проверок: список бэкендов, отказ с
перечислением, выбор metal, переменная R2D_GPU), полный набор qjs и агентских
тестов зелёный. Пункт 5 плана (§12) закрыт целиком.
владелец 6633e54a
......@@ -81,6 +81,49 @@ $.window.cursor('hidden');
$.window.on('resize', ({ w, h }) => relayout(w, h));
```
## Выбор GPU-бэкенда
Движок рисует через SDL3 GPU и включает все три формата шейдеров (SPIR-V, MSL,
DXIL); конкретный бэкенд SDL выбирает сам. Если нужно назвать его вручную — при
отладке драйверов или чтобы обойти проблемный бэкенд:
```bash
# Что собрано в этой сборке
./build/russiano2d --list-gpu
# Выбрать бэкенд
./build/russiano2d --gpu metal
./build/russiano2d --gpu vulkan
R2D_GPU=vulkan ./build/russiano2d # то же переменной окружения
```
Имена: `metal`, `vulkan`, `direct3d12` (принимается и привычное `d3d12`).
Если бэкенд недоступен, движок **не открывает пустое окно**, а пишет, что
именно не получилось, и перечисляет собранные бэкенды:
```text
[error] SDL_CreateGPUDevice("vulkan"): SDL_HINT_GPU_DRIVER vulkan unsupported!
[error] бэкенд "vulkan" недоступен в этой сборке или на этой машине.
Собранные бэкенды: metal, vulkan
```
Отдельно про **D3D12**: он работает только с DXIL, поэтому нужна сборка с
поддержкой DXIL (DXC). Без неё отказ дополняется подсказкой:
```text
[error] D3D12 работает только с DXIL: нужна сборка движка с поддержкой DXIL
(DXC) либо другой бэкенд — --gpu vulkan или --gpu metal
```
Почему так, а не «просто не запуститься». Раньше при неудаче печаталась одна
строка `SDL_CreateGPUDevice: …` без имени бэкенда и без списка доступных: на
машине с одним рабочим бэкендом приходилось угадывать. Теперь отказ называет
бэкенд, перечисляет альтернативы и не оставляет окна-зомби.
**Имя бэкенда задаётся при инициализации приложения** (`r2d_app_init` получает
его аргументом): структура приложения обнуляется внутри инициализации, поэтому
записать поле до вызова было бы недостаточно.
## Режимы сборки
**`--append` (по умолчанию).** Билдер копирует движок и приписывает к копии
......
......@@ -728,10 +728,10 @@ guide-примеров не работает. Ниже — точные мест
тайлов, blend в `layers/tilemap`.
4. **Ресурсы:** выгрузка текстур и слотов, импорт атласов, фильтрация/мипмапы,
Curve/Gradient как ресурсы.
5. **Платформа и асинхронность:** ~~фоновая загрузка~~ **сделано** (`$.task` +
`$.scene.loadAsync`, task.md); ~~неблокирующий hot reload~~ **сделано**
(`$.script`: перезапуск на границе кадра, script.md); осталось:
выбор GPU-бэкенда, отказ D3D12 без DXIL с понятной ошибкой.
5. **Платформа и асинхронность:** **закрыто.** Фоновая загрузка — `$.task` +
`$.scene.loadAsync` (task.md); неблокирующий hot reload — `$.script`
(перезапуск на границе кадра, script.md); выбор GPU-бэкенда и понятный отказ
D3D12 без DXIL — `--gpu`/`--list-gpu`/`R2D_GPU` (BUILD.md).
6. **Жанровое (P2):** CCD, суставы prismatic/wheel/pulley/gear/mouse, контакты по
формам и импульсы, вращение камеры, мультикамерность, touch, мультигеймпад,
clipboard/IME, клипы локализации и плюрализация, контролы и виртуализация
......
......@@ -48,9 +48,13 @@ static void r2d__pick_base_path(R2DApp *app)
app->base_path_owned = app->base_path != NULL;
}
bool r2d_app_init(R2DApp *app, const char *title, int width, int height, bool vsync, bool headless)
bool r2d_app_init(R2DApp *app, const char *title, int width, int height, bool vsync,
bool headless, const char *gpu_driver)
{
SDL_zero(*app);
// Имя бэкенда задаётся СРАЗУ: SDL_zero обнуляет структуру, поэтому
// записать его до вызова было бы недостаточно.
if (gpu_driver) SDL_strlcpy(app->gpu_driver, gpu_driver, sizeof app->gpu_driver);
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD | SDL_INIT_AUDIO)) {
R2D_ERROR("SDL_Init: %s", SDL_GetError());
......@@ -90,9 +94,41 @@ bool r2d_app_init(R2DApp *app, const char *title, int width, int height, bool vs
const bool gpu_debug = true;
#endif
app->device = SDL_CreateGPUDevice(formats, gpu_debug, NULL);
// Выбор бэкенда: `--gpu vulkan` или R2D_GPU. Без явного выбора SDL берёт
// первый подходящий — обычно это и нужно; при отладке важно уметь назвать
// бэкенд и получить понятный отказ вместо пустого окна.
const char *requested = (app->gpu_driver[0] != '\0') ? app->gpu_driver
: SDL_getenv("R2D_GPU");
if (requested && requested[0] == '\0') requested = NULL;
if (requested && SDL_strcasecmp(requested, "d3d12") == 0) requested = "direct3d12";
if (requested) {
app->device = SDL_CreateGPUDevice(formats, gpu_debug, requested);
if (!app->device) {
// Не «неизвестное имя», а «этот бэкенд здесь не работает»: SDL
// знает имя (оно есть в списке), но создать устройство не смог.
R2D_ERROR("SDL_CreateGPUDevice(\"%s\"): %s", requested, SDL_GetError());
const int count = SDL_GetNumGPUDrivers();
char list[256] = "";
for (int i = 0; i < count; ++i) {
const char *name = SDL_GetGPUDriver(i);
if (!name) continue;
SDL_strlcat(list, name, sizeof list);
if (i + 1 < count) SDL_strlcat(list, ", ", sizeof list);
}
R2D_ERROR("бэкенд \"%s\" недоступен в этой сборке или на этой машине. "
"Собранные бэкенды: %s", requested, count > 0 ? list : "(ни одного)");
if (SDL_strcasecmp(requested, "direct3d12") == 0) {
R2D_ERROR("D3D12 работает только с DXIL: нужна сборка движка с поддержкой "
"DXIL (DXC) либо другой бэкенд — --gpu vulkan или --gpu metal");
}
}
} else {
app->device = SDL_CreateGPUDevice(formats, gpu_debug, NULL);
}
if (!app->device) {
R2D_ERROR("SDL_CreateGPUDevice: %s", SDL_GetError());
if (!requested) R2D_ERROR("SDL_CreateGPUDevice: %s", SDL_GetError());
return false;
}
......
......@@ -24,6 +24,7 @@ typedef struct R2DApp {
char title[256]; // текущее имя окна (его можно менять на ходу)
bool vsync; // вертикальная синхронизация презентации
char cursor[16]; // вид курсора: normal | hidden | crosshair | hand
char gpu_driver[32]; // запрошенный GPU-бэкенд ("" — выбрать автоматически)
int width; // логический размер (в точках)
int height;
......@@ -105,7 +106,9 @@ typedef struct R2DApp {
// Создаёт окно и GPU-устройство. Возвращает false и печатает причину при ошибке.
// headless — окно создаётся скрытым (агентский режим, CI): рендер, GPU-проход и
// скриншоты работают, но на экране ничего не появляется.
bool r2d_app_init(R2DApp *app, const char *title, int width, int height, bool vsync, bool headless);
// gpu_driver — имя бэкенда ("vulkan", "metal", "d3d12", "" — автоматически).
bool r2d_app_init(R2DApp *app, const char *title, int width, int height, bool vsync,
bool headless, const char *gpu_driver);
// --- Окно -------------------------------------------------------------------
//
......
......@@ -87,6 +87,8 @@ static void r2d__print_usage(const char *exe)
" --seconds <N> выйти автоматически через N секунд (для тестов)\n"
" --frames <N> выйти ровно после N кадров\n"
" --no-hot-reload не следить за изменениями .js\n"
" --gpu <имя> GPU-бэкенд: vulkan, d3d12, metal\n"
" --list-gpu показать доступные бэкенды и выйти\n"
"\n"
" --- Режим агента (см. docs/AGENT_API.md) ---\n"
" --agent читать JSON-команды со stdin и отвечать в stdout\n"
......@@ -506,6 +508,7 @@ int main(int argc, char **argv)
const char *opt_title = NULL; // --title: своё имя окна
int opt_width = 0; // --width/--height: свой размер окна
int opt_height = 0;
const char *opt_gpu = NULL; // --gpu: выбрать бэкенд вручную
const char *opt_screenshot = NULL;
double opt_screenshot_at = 2.0;
bool opt_overlay = false;
......@@ -522,6 +525,18 @@ int main(int argc, char **argv)
opt_agent = true;
} else if (SDL_strcmp(argv[i], "--headless") == 0) {
opt_headless = true;
} else if (SDL_strcmp(argv[i], "--gpu") == 0 && i + 1 < argc) {
opt_gpu = argv[++i];
} else if (SDL_strcmp(argv[i], "--list-gpu") == 0) {
// Печатаем бэкенды и выходим: полезно, когда окно не открывается.
const int count = SDL_GetNumGPUDrivers();
SDL_Log("GPU-бэкенды этой сборки:");
for (int d = 0; d < count; ++d) {
const char *name = SDL_GetGPUDriver(d);
if (name) SDL_Log(" %s", name);
}
if (count == 0) SDL_Log(" (ни одного)");
return 0;
} else if (SDL_strcmp(argv[i], "--fixed-dt") == 0 && i + 1 < argc) {
opt_fixed_dt = SDL_atof(argv[++i]);
} else if (SDL_strcmp(argv[i], "--seed") == 0 && i + 1 < argc) {
......@@ -590,7 +605,10 @@ int main(int argc, char **argv)
const int window_width = opt_width > 0 ? opt_width : project.width;
const int window_height = opt_height > 0 ? opt_height : project.height;
if (!r2d_app_init(&app, window_title, window_width, window_height, true, opt_headless)) {
// Имя бэкенда передаём в инициализацию: там структура обнуляется, и
// записать его заранее было бы недостаточно.
if (!r2d_app_init(&app, window_title, window_width, window_height, true, opt_headless,
opt_gpu)) {
r2d_app_shutdown(&app);
return 1;
}
......
#!/usr/bin/env python3
# ===========================================================================
# Тест выбора GPU-бэкенда из командной строки.
#
# Проверяется поведение, которое видно только снаружи движка: список бэкендов,
# понятный отказ на недоступный бэкенд (а не пустое окно) и рабочий выбор
# своего бэкенда на этой машине.
#
# Запуск после сборки:
# python3 tests/agent/cli_gpu_test.py
# ===========================================================================
import os
import re
import subprocess
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "tools"))
from agent_client import resolve_binary # noqa: E402
FAILURES = []
GAME = os.path.join("tests", "fixtures", "text")
def check(condition, message):
print((" ok " if condition else " FAIL ") + message)
if not condition:
FAILURES.append(message)
def run(args):
proc = subprocess.run([resolve_binary()] + args, capture_output=True, text=True, timeout=120)
return (proc.stdout or "") + (proc.stderr or "")
def main():
listing = run(["--list-gpu"])
names = re.findall(r"\]\s+(metal|vulkan|direct3d12)\s*$", listing, re.M)
check(len(names) >= 1, f"--list-gpu показывает бэкенды: {names}")
bad = run(["--gpu", "нет-такого", "--game", GAME, "--headless", "--frames", "1"])
check("недоступен" in bad or "unsupported" in bad,
"неизвестный бэкенд отвергнут с объяснением")
check(any(n in bad for n in names) or "ни одного" in bad,
"в отказе перечислены доступные бэкенды")
if "metal" in names:
good = run(["--gpu", "metal", "--game", GAME, "--headless", "--frames", "2"])
check("GPU-бэкенд: metal" in good, "выбор metal работает")
env = dict(os.environ, R2D_GPU=names[0])
proc = subprocess.run([resolve_binary(), "--game", GAME, "--headless", "--frames", "2"],
capture_output=True, text=True, timeout=120, env=env)
check(f"GPU-бэкенд: {names[0]}" in (proc.stdout or "") + (proc.stderr or ""),
"переменная R2D_GPU тоже выбирает бэкенд")
print()
if FAILURES:
print(f"ПРОВАЛОВ: {len(FAILURES)}")
for f in FAILURES:
print(" - " + f)
return 1
print("Все проверки пройдены")
return 0
if __name__ == "__main__":
sys.exit(main())
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать