OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / rtx_test.gd

530 lines · gd ref:
extends Node

## RTX Feature Test Harness
## Cycles through all path tracing features to verify they work on Linux/RTX 2070
##
## Controls:
##   1-9: Set visualization mode (0=full PT, 1=mirror, 2=geo normals, etc.)
##   0:   Full path tracing (default)
##   B:   Cycle max bounces (1-4)
##   S:   Cycle samples per pixel (1, 2, 4)
##   R:   Toggle raytracing on/off
##   P:   Print current RT params
##   N:   Cycle DLSS mode (see below)
##   [/]: Adjust sharpness (DLSS-SR only)
##   F5:  Run automated feature test (cycles all modes)
##   F12: Take screenshot to /tmp/rtx_screenshots/

# DLSS modes — ordered from fastest to highest quality
enum DLSSMode { OFF, SR_PERF, SR_QUALITY, RR_PERF, RR_QUALITY, RR_DLAA }
const DLSS_MODE_NAMES = {
	DLSSMode.OFF: "OFF",
	DLSSMode.SR_PERF: "SR Performance (50%)",
	DLSSMode.SR_QUALITY: "SR Quality (67%)",
	DLSSMode.RR_PERF: "RR Performance (50%)",
	DLSSMode.RR_QUALITY: "RR Quality (67%)",
	DLSSMode.RR_DLAA: "RR DLAA (native res)",
}

@export var start_pt_enabled: bool = true

var env: Environment
var screenshot_count: int = 0
var dlss_mode: int = DLSSMode.OFF
var current_vis_mode: int = 0
var current_bounces: int = 1
var current_samples: int = 1
var current_sharpness: float = 0.5
var auto_test_running: bool = false
var auto_test_step: int = 0
var auto_test_timer: float = 0.0
var use_legacy_api: bool = false  # true = old raytracing_* API, false = new pathtracing_* API

const VIS_MODE_NAMES = {
	0: "Full Path Tracing",
	1: "Mirror Reflection",
	2: "Geometry Normals",
	3: "Final Normals (w/ normal map)",
	4: "Tangent-Space Normal Map",
	5: "Tangent",
	6: "Bitangent",
	7: "UV Visualization",
	8: "Albedo Only",
	9: "ORM Texture",
	10: "DLSS-RR Diffuse Albedo",
	11: "DLSS-RR Specular Albedo",
	12: "DLSS-RR Normal+Roughness",
	13: "DLSS-RR Specular Hit Dist",
	14: "Metalness",
	15: "Roughness",
	16: "View-Space Normals",
	17: "Diffuse+Specular Split",
	18: "Fresnel F0",
	19: "Front/Back Face",
}

func _ready() -> void:
	# Wait for environment to be available
	await get_tree().process_frame
	await get_tree().process_frame

	var we := _find_world_environment()
	if we and we.environment:
		env = we.environment
		# Detect API version: new builds have pathtracing_enabled, old have raytracing_enabled
		use_legacy_api = true
		for prop in env.get_property_list():
			if prop["name"] == "pathtracing_enabled":
				use_legacy_api = false
				break
		if use_legacy_api:
			print("[RTX Test] Using LEGACY API (raytracing_*)")
		_init_rt_params()
		print("[RTX Test] Environment found. Pathtracing: %s" % _get_rt_enabled())
		print("[RTX Test] Controls: 0-9=vis, B=bounces, S=samples, R=toggle RT, N=cycle DLSS, P=status, F12=screenshot")
		# Start with DLSS off to avoid NGX errors on broken drivers
		_set_dlss_mode(DLSSMode.OFF)
		# Auto-start benchmark if --benchmark flag passed
		if "--benchmark" in OS.get_cmdline_user_args():
			print("[RTX Benchmark] Auto-start via --benchmark flag")
			# Wait a few frames for everything to settle
			await get_tree().create_timer(1.0).timeout
			_start_auto_test()
	else:
		push_error("[RTX Test] No WorldEnvironment found!")


func _init_rt_params() -> void:
	# Enable RT (removed from tscn for dual-API compatibility)
	_set_rt_enabled(start_pt_enabled)
	if use_legacy_api:
		var rt_params := PackedFloat64Array()
		rt_params.resize(16)
		rt_params[0] = 0   # debug mode
		rt_params[1] = 1   # spp
		rt_params[2] = 1   # bounces
		rt_params[3] = 0   # denoiser (was DLSS_RR_ENABLED)
		rt_params[4] = 0   # light count (old index, now at 14)
		env.set("raytracing_params", rt_params)
	else:
		env.pathtracing_debug_mode = 0
		env.pathtracing_samples_per_pixel = 1
		env.pathtracing_max_bounces = 1
		env.pathtracing_denoiser = 0
	_print_status()


# --- Dual API helpers ---

func _get_rt_enabled() -> bool:
	if use_legacy_api:
		return env.get("raytracing_enabled")
	return env.pathtracing_enabled


func _set_rt_enabled(val: bool) -> void:
	if use_legacy_api:
		env.set("raytracing_enabled", val)
	else:
		env.pathtracing_enabled = val


func _set_debug_mode(mode: int) -> void:
	if use_legacy_api:
		var p = env.get("raytracing_params")
		p[0] = mode
		env.set("raytracing_params", p)
	else:
		env.pathtracing_debug_mode = mode


func _set_spp(spp: int) -> void:
	if use_legacy_api:
		var p = env.get("raytracing_params")
		p[1] = spp
		env.set("raytracing_params", p)
	else:
		env.pathtracing_samples_per_pixel = spp


func _set_bounces(bounces: int) -> void:
	if use_legacy_api:
		var p = env.get("raytracing_params")
		p[2] = bounces
		env.set("raytracing_params", p)
	else:
		env.pathtracing_max_bounces = bounces


func _set_denoiser(val: int) -> void:
	if use_legacy_api:
		var p = env.get("raytracing_params")
		p[3] = val
		env.set("raytracing_params", p)
	else:
		env.pathtracing_denoiser = val


func _input(event: InputEvent) -> void:
	if not env:
		return
	if event is InputEventKey and event.pressed and not event.echo:
		match event.keycode:
			# Number keys 0-9 for vis modes
			KEY_0: _set_vis_mode(0)
			KEY_1: _set_vis_mode(1)
			KEY_2: _set_vis_mode(2)
			KEY_3: _set_vis_mode(3)
			KEY_4: _set_vis_mode(4)
			KEY_5: _set_vis_mode(5)
			KEY_6: _set_vis_mode(6)
			KEY_7: _set_vis_mode(7)
			KEY_8: _set_vis_mode(8)
			KEY_9: _set_vis_mode(9)
			KEY_B: _cycle_bounces()
			KEY_S: _cycle_samples()
			KEY_R: _toggle_raytracing()
			KEY_N: _cycle_dlss()
			KEY_P: _print_status()
			KEY_BRACKETLEFT: _adjust_sharpness(-0.1)
			KEY_BRACKETRIGHT: _adjust_sharpness(0.1)
			KEY_F12: _take_screenshot()


func _set_vis_mode(mode: int) -> void:
	current_vis_mode = mode
	_set_debug_mode(mode)
	var name = VIS_MODE_NAMES.get(mode, "Unknown")
	print("[RTX Test] Vis Mode: %d (%s)" % [mode, name])


func _cycle_bounces() -> void:
	current_bounces = (current_bounces % 4) + 1
	_set_bounces(current_bounces)
	print("[RTX Test] Max Bounces: %d" % current_bounces)


func _cycle_samples() -> void:
	match current_samples:
		1: current_samples = 2
		2: current_samples = 4
		_: current_samples = 1
	_set_spp(current_samples)
	print("[RTX Test] Samples Per Pixel: %d" % current_samples)


func _toggle_raytracing() -> void:
	_set_rt_enabled(not _get_rt_enabled())
	print("[RTX Test] Pathtracing: %s" % ("ON" if _get_rt_enabled() else "OFF"))


func _adjust_sharpness(delta: float) -> void:
	current_sharpness = clampf(current_sharpness + delta, 0.0, 1.0)
	get_viewport().fsr_sharpness = current_sharpness * 2.0  # Viewport range is 0-2
	print("[RTX Test] Sharpness: %.1f (fsr=%.1f)" % [current_sharpness, current_sharpness * 2.0])


func _cycle_dlss() -> void:
	_set_dlss_mode((dlss_mode + 1) % DLSSMode.size())


func _set_dlss_mode(mode: int) -> void:
	dlss_mode = mode
	var viewport := get_viewport()
	match mode:
		DLSSMode.OFF:
			viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_BILINEAR
			viewport.scaling_3d_scale = 1.0
			_set_denoiser(0)
		DLSSMode.SR_PERF:
			viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_DLSS
			viewport.scaling_3d_scale = 0.5
			_set_denoiser(0)
		DLSSMode.SR_QUALITY:
			viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_DLSS
			viewport.scaling_3d_scale = 0.67
			_set_denoiser(0)
		DLSSMode.RR_PERF:
			viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_DLSS
			viewport.scaling_3d_scale = 0.5
			_set_denoiser(1)
		DLSSMode.RR_QUALITY:
			viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_DLSS
			viewport.scaling_3d_scale = 0.67
			_set_denoiser(1)
		DLSSMode.RR_DLAA:
			viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_DLSS
			viewport.scaling_3d_scale = 1.0
			_set_denoiser(1)
	print("[RTX Test] DLSS: %s" % DLSS_MODE_NAMES[mode])


func _print_status() -> void:
	print("=== RTX Status ===")
	print("  Pathtracing Enabled: %s" % _get_rt_enabled())
	print("  Vis Mode: %d (%s)" % [current_vis_mode, VIS_MODE_NAMES.get(current_vis_mode, "?")])
	print("  Samples/Pixel: %d" % current_samples)
	print("  Max Bounces: %d" % current_bounces)
	print("  DLSS: %s" % DLSS_MODE_NAMES[dlss_mode])
	print("  FPS: %.1f" % Engine.get_frames_per_second())
	# Check RT capability
	var rd = RenderingServer.get_rendering_device()
	if rd:
		var has_rt = rd.has_feature(RenderingDevice.SUPPORTS_RAYTRACING_PIPELINE)
		var has_accel = rd.has_feature(RenderingDevice.SUPPORTS_RAY_QUERY)
		print("  VK RT Pipeline: %s" % has_rt)
		print("  VK Ray Query: %s" % has_accel)
	print("==================")


func _start_auto_test() -> void:
	if auto_test_running:
		print("[RTX Test] Auto test already running")
		return
	print("[RTX Benchmark] Starting exhaustive benchmark...")
	print("[RTX Benchmark] Disabling VSync for uncapped measurements")
	print("[RTX Benchmark] Each config runs for 4s (1s warmup + 3s measure)")
	DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
	auto_test_running = true
	auto_test_step = 0
	auto_test_timer = 0.0
	bench_results.clear()
	bench_frame_times.clear()
	bench_warmup = true
	_bench_apply_config(0)


# Benchmark state
var bench_results: Array = []
var bench_frame_times: Array[float] = []
var bench_warmup: bool = true
const BENCH_WARMUP_TIME := 1.0
const BENCH_MEASURE_TIME := 3.0

# Benchmark configurations: [description, rt_on, bounces, spp, dlss_mode]
var bench_configs: Array = [
	# Rasterizer baseline
	["Rasterized (Forward+)", false, 0, 0, DLSSMode.OFF],
	# RT basics — bounces sweep
	["RT 1spp 1bounce NO DLSS", true, 1, 1, DLSSMode.OFF],
	["RT 1spp 2bounce NO DLSS", true, 2, 1, DLSSMode.OFF],
	["RT 1spp 3bounce NO DLSS", true, 3, 1, DLSSMode.OFF],
	["RT 1spp 4bounce NO DLSS", true, 4, 1, DLSSMode.OFF],
	# RT basics — spp sweep
	["RT 2spp 2bounce NO DLSS", true, 2, 2, DLSSMode.OFF],
	["RT 4spp 2bounce NO DLSS", true, 2, 4, DLSSMode.OFF],
	# DLSS-SR
	["RT 1spp 1b SR Perf 50%", true, 1, 1, DLSSMode.SR_PERF],
	["RT 1spp 1b SR Quality 67%", true, 1, 1, DLSSMode.SR_QUALITY],
	["RT 1spp 2b SR Perf 50%", true, 2, 1, DLSSMode.SR_PERF],
	# DLSS-RR
	["RT 1spp 1b RR Perf 50%", true, 1, 1, DLSSMode.RR_PERF],
	["RT 1spp 1b RR Quality 67%", true, 1, 1, DLSSMode.RR_QUALITY],
	["RT 1spp 1b RR DLAA native", true, 1, 1, DLSSMode.RR_DLAA],
	# SKIP: RT 1spp 2b RR — vkCreateRayTracingPipelinesKHR fails with VK_ERROR_INITIALIZATION_FAILED
	# on RTX 2070. Pipeline can't be created with 2-bounce + RR shader permutation (driver bug).
	# ["RT 1spp 2b RR Perf 50%", true, 2, 1, DLSSMode.RR_PERF],
	# ["RT 1spp 2b RR Quality 67%", true, 2, 1, DLSSMode.RR_QUALITY],
	["RT 2spp 2b RR Perf 50%", true, 2, 2, DLSSMode.RR_PERF],
]


func _process(delta: float) -> void:
	if not auto_test_running:
		return

	auto_test_timer += delta

	if bench_warmup:
		# Warmup phase — let DLSS/RT stabilize
		if auto_test_timer >= BENCH_WARMUP_TIME:
			auto_test_timer = 0.0
			bench_warmup = false
			bench_frame_times.clear()
		return

	# Measurement phase — collect frame times
	bench_frame_times.append(delta * 1000.0)  # ms

	if auto_test_timer >= BENCH_MEASURE_TIME:
		# Record results for this config
		_bench_record_result()

		# Move to next config
		auto_test_step += 1
		if auto_test_step >= bench_configs.size():
			_bench_finish()
			return

		# Apply next config
		_bench_apply_config(auto_test_step)
		auto_test_timer = 0.0
		bench_warmup = true
		bench_frame_times.clear()


func _bench_apply_config(idx: int) -> void:
	var cfg = bench_configs[idx]
	var desc: String = cfg[0]
	var rt_on: bool = cfg[1]
	var bounces: int = cfg[2]
	var spp: int = cfg[3]
	var dlss: int = cfg[4]

	print("[RTX Benchmark %d/%d] Setting up: %s" % [idx + 1, bench_configs.size(), desc])

	if rt_on and not _get_rt_enabled():
		_toggle_raytracing()
	elif not rt_on and _get_rt_enabled():
		_toggle_raytracing()

	if rt_on:
		_set_vis_mode(0)
		current_samples = spp
		current_bounces = bounces
		_set_spp(spp)
		_set_bounces(bounces)

	_set_dlss_mode(dlss)


func _bench_record_result() -> void:
	if bench_frame_times.is_empty():
		return

	var cfg = bench_configs[auto_test_step]
	var desc: String = cfg[0]

	# Sort for percentiles
	var sorted: Array = bench_frame_times.duplicate()
	sorted.sort()

	var count := sorted.size()
	var sum := 0.0
	for t in sorted:
		sum += t

	var avg_ms := sum / count
	var avg_fps := 1000.0 / avg_ms
	var min_ms: float = sorted[0]
	var max_ms: float = sorted[count - 1]
	var p1_ms: float = sorted[int(count * 0.01)]
	var p50_ms: float = sorted[int(count * 0.50)]
	var p99_ms: float = sorted[int(count * 0.99)]

	var result := {
		"config": desc,
		"avg_fps": avg_fps,
		"avg_ms": avg_ms,
		"min_ms": min_ms,
		"max_ms": max_ms,
		"p1_ms": p1_ms,
		"p50_ms": p50_ms,
		"p99_ms": p99_ms,
		"frames": count,
	}
	bench_results.append(result)

	var stall_tag := ""
	if avg_fps < 10.0:
		stall_tag = " *** STALL (%.1f FPS < 10) ***" % avg_fps
	elif p99_ms > 100.0:
		stall_tag = " ** HIGH P99 (%.0f ms) **" % p99_ms
	print("[RTX Benchmark] %s: %.1f FPS (avg %.1f ms, p1 %.1f, p50 %.1f, p99 %.1f, %d frames)%s" % [
		desc, avg_fps, avg_ms, p1_ms, p50_ms, p99_ms, count, stall_tag])


func _bench_finish() -> void:
	auto_test_running = false
	DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
	print("[RTX Benchmark] VSync re-enabled")

	# Print summary table
	print("")
	print("=" .repeat(100))
	print("[RTX Benchmark] RESULTS — %s" % Time.get_datetime_string_from_system())
	print("=" .repeat(100))
	print("%-40s %8s %8s %8s %8s %8s %6s" % ["Configuration", "Avg FPS", "Avg ms", "P1% ms", "P50 ms", "P99 ms", "Frames"])
	print("-" .repeat(100))
	for r in bench_results:
		print("%-40s %8.1f %8.1f %8.1f %8.1f %8.1f %6d" % [
			r.config, r.avg_fps, r.avg_ms, r.p1_ms, r.p50_ms, r.p99_ms, r.frames])
	print("=" .repeat(100))

	# Save to /tmp (ephemeral) and to permanent benchmark storage.
	var timestamp := Time.get_datetime_string_from_system().replace(":", "-")
	var label := ""
	for arg in OS.get_cmdline_user_args():
		if arg.begins_with("--label="):
			label = arg.substr(8)
	var basename := "rtx_benchmark_%s%s" % [timestamp, ("_" + label) if label else ""]
	var tmp_path := "/tmp/%s.txt" % basename
	var perm_dir := "/storage/projects/code/godot/nvidia-rtx-godot/benchmarks"
	DirAccess.make_dir_recursive_absolute(perm_dir)

	for path in [tmp_path, "%s/%s.txt" % [perm_dir, basename]]:
		var f := FileAccess.open(path, FileAccess.WRITE)
		if f:
			f.store_line("RTX Benchmark Results — %s" % Time.get_datetime_string_from_system())
			f.store_line("Scene: global_illumination demo")
			f.store_line("")
			f.store_line("%-40s %8s %8s %8s %8s %8s %6s" % ["Configuration", "Avg FPS", "Avg ms", "P1% ms", "P50 ms", "P99 ms", "Frames"])
			f.store_line("-" .repeat(100))
			for r in bench_results:
				f.store_line("%-40s %8.1f %8.1f %8.1f %8.1f %8.1f %6d" % [
					r.config, r.avg_fps, r.avg_ms, r.p1_ms, r.p50_ms, r.p99_ms, r.frames])
			f.close()
			print("[RTX Benchmark] Saved to: %s" % path)

	# Also save as CSV (both locations).
	for path in [tmp_path.replace(".txt", ".csv"), "%s/%s.csv" % [perm_dir, basename]]:
		var csv := FileAccess.open(path, FileAccess.WRITE)
		if csv:
			csv.store_line("config,avg_fps,avg_ms,min_ms,max_ms,p1_ms,p50_ms,p99_ms,frames")
			for r in bench_results:
				csv.store_line("%s,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%d" % [
					r.config, r.avg_fps, r.avg_ms, r.min_ms, r.max_ms, r.p1_ms, r.p50_ms, r.p99_ms, r.frames])
			csv.close()
			print("[RTX Benchmark] CSV saved to: %s" % path)

	# Restore defaults
	_set_dlss_mode(DLSSMode.RR_PERF)

	# Auto-quit after benchmark completes
	print("[RTX Benchmark] Done. Exiting.")
	get_tree().quit()


func _find_world_environment() -> WorldEnvironment:
	for node in get_tree().root.get_children():
		var we := _search_for_world_env(node)
		if we:
			return we
	return null


func _search_for_world_env(node: Node) -> WorldEnvironment:
	if node is WorldEnvironment:
		return node
	for child in node.get_children():
		var result := _search_for_world_env(child)
		if result:
			return result
	return null




func _take_screenshot() -> void:
	var dir := "/tmp/rtx_screenshots"
	DirAccess.make_dir_absolute(dir)
	var image := get_viewport().get_texture().get_image()
	var timestamp := Time.get_datetime_string_from_system().replace(":", "-")
	var mode_name : String = str(DLSS_MODE_NAMES[dlss_mode]).to_lower().replace(" ", "-").replace("(", "").replace(")", "").replace("%", "pct")
	var vis_name : String = str(VIS_MODE_NAMES.get(current_vis_mode, "mode%d" % current_vis_mode)).to_lower().replace(" ", "-")
	var filename := "%s/rtx_%s_%s_%dspp_%db_%s_%03d.png" % [
		dir, timestamp, mode_name, current_samples, current_bounces, vis_name, screenshot_count]
	image.save_png(filename)
	screenshot_count += 1
	print("[RTX Test] Screenshot saved: %s" % filename)