Files
metabuilder/gameengine/generate_cmake.py
johndoe6345789 7ac5ef1d20 feat(gameengine): Windows/AMD build, SPIRV shaders, spotlight, volumetric beam, FPS flashlight
Build system:
- Fix generate_cmake.py backslash paths and UTF-8 encoding for Windows
- Auto C++20 in conan deps, auto-detect VS install location
- dev_commands.py: add generate, all --run commands, platform-aware bootstrap
- Add assimp to cmake_config.json link libraries
- Fix CMakeUserPresets.json duplicate preset issue

Cross-platform C++:
- UUID generation: Windows rpc.h/UuidCreate with #ifdef _WIN32
- HOME env var fallback to USERPROFILE on Windows
- Shader format detection for D3D12/DXIL Vulkan driver

Shader pipeline (12 new SPIRV shaders):
- Port all Metal shaders to Vulkan GLSL (PBR, shadows, post-FX, compute)
- SDL3 GPU descriptor set convention (set 0-3)
- Combined image samplers for Vulkan compatibility
- Bootstrap-driven shader path rewriting (msl↔spirv automatic per platform)

Rendering features:
- spotlight.setup: generic atomic workflow step, attach to camera or static
- PBR spotlight with cone attenuation, distance falloff, wrap lighting
- Volumetric light beam (16-step ray march through dust/fog in spotlight cone)
- geometry.create_flashlight: procedural flashlight mesh (cylinder + head + lens)
- draw.viewmodel: FPS weapon-style rendering locked to camera view
- model.load: Assimp-based 3D model loader (OBJ/GLB/FBX/BLEND)
- Indoor ambient lighting fix, SSAO bypass for Vulkan clip-space

Performance:
- Frame loop logging suppressed via _in_frame_loop context flag

Assets:
- Real PBR textures from ambientCG (CC0): wood floor, concrete ceiling
- Seed demo: dark room + flashlight beam + Quake-style viewmodel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 08:33:35 +00:00

265 lines
8.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
CMakeLists.txt Generator from JSON Configuration
This script generates CMakeLists.txt from a JSON configuration file using Jinja2.
Benefits:
• Removes 1500+ lines of repetitive CMake code
• Makes dependencies and targets declarative (JSON)
• Easy to add new targets without CMake expertise
• Modular: include files for dependencies, targets, tests
• Version controllable: JSON changes are easier to review than CMake
Usage:
python3 generate_cmake.py --config cmake_config.json --template CMakeLists.txt.jinja2 --output CMakeLists.txt
Example JSON structure:
{
"project": { "name": "MyProject", "cmake_minimum_version": "3.24" },
"options": [
{ "name": "OPTION_NAME", "type": "BOOL", "default": "ON" }
],
"targets": [
{ "name": "my_app", "type": "executable", "sources": [...] }
],
"test_targets": [
{ "name": "test_something", "sources": [...] }
]
}
"""
import json
import argparse
import sys
from pathlib import Path
from glob import glob
from os import path
try:
from jinja2 import Environment, FileSystemLoader, select_autoescape
except ImportError:
print("ERROR: jinja2 is required. Install with: pip install jinja2")
sys.exit(1)
def load_config(config_path: str) -> dict:
"""Load configuration from JSON file."""
with open(config_path, 'r') as f:
return json.load(f)
def _posix_path(p: str) -> str:
"""Normalize path separators to forward slashes for CMake compatibility."""
return p.replace('\\', '/')
def expand_test_glob_patterns(config: dict) -> dict:
"""Expand glob patterns in test_targets list itself.
Supports:
- "pattern": "tests/unit/**/*.cpp" - generates test from each cpp file
- Can be mixed with explicit test definitions
"""
exclusions = set(config.get('source_exclusions', []))
expanded_tests = []
for test_def in config.get('test_targets', []):
if isinstance(test_def, dict) and 'pattern' in test_def:
# This is a glob pattern definition - expand it
pattern = test_def['pattern']
matches = sorted(_posix_path(m) for m in glob(pattern))
matches = [m for m in matches if m not in exclusions]
template = test_def.copy()
del template['pattern'] # Remove pattern key
for match in matches:
# Create a test target from each match
test_name = Path(match).stem # e.g., test_audio_play_step
test = template.copy()
test['name'] = test_name
test['sources'] = [match] + template.get('sources', [])[1:] # Replace first source with match
expanded_tests.append(test)
else:
# Regular test definition
expanded_tests.append(test_def)
config['test_targets'] = expanded_tests
return config
def expand_globs(config: dict) -> dict:
"""Expand glob patterns in source lists and apply exclusions."""
# First expand test_targets from glob patterns
config = expand_test_glob_patterns(config)
exclusions = set(config.get('source_exclusions', []))
for target in config.get('targets', []):
if 'sources' in target:
expanded = []
for src in target['sources']:
if '*' in src:
# Glob pattern - expand it
matches = sorted(_posix_path(m) for m in glob(src))
# Filter out excluded files
matches = [m for m in matches if m not in exclusions]
expanded.extend(matches)
elif src not in exclusions: # Check literal sources too
expanded.append(_posix_path(src))
target['sources'] = expanded
for test in config.get('test_targets', []):
if 'sources' in test:
expanded = []
for src in test['sources']:
if '*' in src:
matches = sorted(_posix_path(m) for m in glob(src))
# Filter out excluded files
matches = [m for m in matches if m not in exclusions]
expanded.extend(matches)
elif src not in exclusions:
expanded.append(_posix_path(src))
test['sources'] = expanded
# Provide smart defaults for test targets from config
if 'include_directories' not in test:
test['include_directories'] = config.get('test_defaults', {}).get('include_directories', ['src'])
if 'link_libraries' not in test or not test['link_libraries']:
# Use configured defaults or standard test libraries
test['link_libraries'] = config.get('test_defaults', {}).get('link_libraries', [
'GTest::gtest_main',
'GTest::gmock',
'glm::glm',
'Bullet::Bullet',
'EnTT::EnTT'
])
return config
def generate_cmake(config: dict, template_path: str, output_path: str) -> None:
"""Generate CMakeLists.txt from config and template."""
# Expand glob patterns in source lists
config = expand_globs(config)
# Setup Jinja2 environment
template_dir = Path(template_path).parent
template_name = Path(template_path).name
env = Environment(
loader=FileSystemLoader(template_dir),
autoescape=select_autoescape(),
trim_blocks=False,
lstrip_blocks=False
)
# Load and render template
template = env.get_template(template_name)
rendered = template.render(config=config)
# Write output
with open(output_path, 'w') as f:
f.write(rendered)
print(f"✓ Generated {output_path}")
print(f" - Project: {config['project']['name']}")
print(f" - Options: {len(config.get('options', []))}")
print(f" - Targets: {len(config.get('targets', []))}")
print(f" - Tests: {len(config.get('test_targets', []))}")
def validate_config(config: dict) -> bool:
"""Validate configuration structure."""
required_keys = ['project']
for key in required_keys:
if key not in config:
print(f"ERROR: Missing required key in config: {key}")
return False
# Validate project structure
project = config['project']
if 'name' not in project or 'cmake_minimum_version' not in project:
print("ERROR: project must have 'name' and 'cmake_minimum_version'")
return False
# Validate targets if present
for target in config.get('targets', []):
if 'name' not in target or 'type' not in target:
print(f"ERROR: target missing 'name' or 'type': {target}")
return False
return True
def main():
parser = argparse.ArgumentParser(
description="Generate CMakeLists.txt from JSON configuration",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 generate_cmake.py \\
--config cmake_config.json \\
--template CMakeLists.txt.jinja2 \\
--output CMakeLists.txt
# Validate config only
python3 generate_cmake.py --config cmake_config.json --validate
"""
)
parser.add_argument(
'--config',
required=True,
help='Path to cmake_config.json'
)
parser.add_argument(
'--template',
default='CMakeLists.txt.jinja2',
help='Path to CMakeLists.txt.jinja2 template (default: CMakeLists.txt.jinja2)'
)
parser.add_argument(
'--output',
default='CMakeLists.txt',
help='Output CMakeLists.txt path (default: CMakeLists.txt)'
)
parser.add_argument(
'--validate',
action='store_true',
help='Validate config without generating'
)
args = parser.parse_args()
# Load and validate config
try:
config = load_config(args.config)
except FileNotFoundError:
print(f"ERROR: Config file not found: {args.config}")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"ERROR: Invalid JSON in {args.config}: {e}")
sys.exit(1)
if not validate_config(config):
sys.exit(1)
if args.validate:
print(f"✓ Config is valid: {args.config}")
return
# Generate CMakeLists.txt
try:
generate_cmake(config, args.template, args.output)
except FileNotFoundError as e:
print(f"ERROR: Template file not found: {args.template}")
sys.exit(1)
except Exception as e:
print(f"ERROR: Failed to generate CMakeLists.txt: {e}")
sys.exit(1)
if __name__ == '__main__':
main()