Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/linux_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ jobs:
run: |
mkdir torch-release
mv build-cmake/torch torch-release/
mv build-cmake/torch-lus-assets.o2r torch-release/
cp "$(ldconfig -p | grep libbz2.so.1.0 | tr ' ' '\n' | grep /| head -n1)" torch-release/
- name: Publish packaged artifacts
if: ${{ matrix.config == 'Release' && matrix.standalone == 'ON' && matrix.toolchain == 'linux-gnu' }}
uses: actions/upload-artifact@v4
with:
name: torch-linux-x64
path: torch-release
path: torch-release
1 change: 1 addition & 0 deletions .github/workflows/mac_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
run: |
mkdir torch-release
mv build-cmake/torch torch-release/
mv build-cmake/torch-lus-assets.o2r torch-release/
- name: Publish packaged artifacts
if: ${{ matrix.config == 'Release' && matrix.standalone == 'ON' && matrix.toolchain == 'macos-clang' }}
uses: actions/upload-artifact@v4
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/windows_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: torch-windows-${{ matrix.arch }}-${{ matrix.toolchain }}
path: build/${{ matrix.arch }}/Release/torch.exe
if-no-files-found: error
path: |
build/${{ matrix.arch }}/Release/torch.exe
build/${{ matrix.arch }}/Release/torch-lus-assets.o2r
if-no-files-found: error
38 changes: 33 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ if(BUILD_UI)
)
FetchContent_MakeAvailable(libultraship)

find_package(Python3 REQUIRED COMPONENTS Interpreter)

include_directories(
${libultraship_SOURCE_DIR}/include
${libultraship_SOURCE_DIR}/include/libultraship
Expand All @@ -127,11 +129,22 @@ if(BUILD_UI)

# The Fast3D Metal/GL shaders are loaded as resources ("shaders/...") and must
# match THIS libultraship's prism context. Game .o2r archives ship their own
# (often fork-modified) shaders, so copy the upstream LUS shaders next to the
# build and mount them last (overriding) at runtime. See LusBackend.cpp.
file(COPY ${libultraship_SOURCE_DIR}/src/fast/shaders
DESTINATION ${CMAKE_BINARY_DIR}/torch-lus-assets)
add_compile_definitions(TORCH_LUS_SHADER_DIR="${CMAKE_BINARY_DIR}/torch-lus-assets")
# (often fork-modified) shaders, so package the upstream LUS shaders and mount
# them last (overriding) at runtime. A file archive also avoids LUS's fragile
# Windows folder-archive path handling. See LusBackend.cpp.
set(TORCH_LUS_SHADER_ROOT ${libultraship_SOURCE_DIR}/src/fast)
set(TORCH_LUS_SHADER_ARCHIVE ${CMAKE_BINARY_DIR}/generated/torch-lus-assets.o2r)
file(GLOB_RECURSE TORCH_LUS_SHADER_FILES CONFIGURE_DEPENDS
LIST_DIRECTORIES false ${TORCH_LUS_SHADER_ROOT}/shaders/*)
add_custom_command(
OUTPUT ${TORCH_LUS_SHADER_ARCHIVE}
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/package_lus_shaders.py
--source ${TORCH_LUS_SHADER_ROOT}
--output ${TORCH_LUS_SHADER_ARCHIVE}
DEPENDS ${TORCH_LUS_SHADER_FILES} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/package_lus_shaders.py
COMMENT "Packaging libultraship shaders"
VERBATIM
)
endif()

# Source files
Expand Down Expand Up @@ -234,6 +247,21 @@ else()
set(LINK_TYPE "MT")
endif()

if(BUILD_UI)
add_custom_target(torch_lus_assets
DEPENDS ${TORCH_LUS_SHADER_ARCHIVE}
)
add_dependencies(${PROJECT_NAME} torch_lus_assets)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:${PROJECT_NAME}>
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${TORCH_LUS_SHADER_ARCHIVE}
$<TARGET_FILE_DIR:${PROJECT_NAME}>/torch-lus-assets.o2r
COMMENT "Installing libultraship shaders next to Torch"
VERBATIM
)
endif()

if (BUILD_STORMLIB)
add_definitions(-DUSE_STORMLIB)
endif()
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ A generic asset processor for games
`./torch otr baserom.z64`
`./torch code baserom.z64`

Viewer builds (`-DBUILD_UI=ON`) also produce `torch-lus-assets.o2r`. Keep that
shader archive beside the Torch executable when copying or packaging it.

# Windows

## Visual Studio
Expand Down
43 changes: 43 additions & 0 deletions cmake/package_lus_shaders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env python3

import argparse
from pathlib import Path
import zipfile


FIXED_TIMESTAMP = (2000, 1, 1, 0, 0, 0)


def main() -> int:
parser = argparse.ArgumentParser(description="Package libultraship shaders reproducibly.")
parser.add_argument("--source", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()

source = args.source.resolve()
shaders = source / "shaders"
files = sorted(path for path in shaders.rglob("*") if path.is_file())
if not files:
parser.error(f"no shader files found below {shaders}")

output = args.output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
temporary = output.with_suffix(output.suffix + ".tmp")

try:
with zipfile.ZipFile(temporary, "w", compression=zipfile.ZIP_STORED) as archive:
for path in files:
name = path.relative_to(source).as_posix()
entry = zipfile.ZipInfo(name, FIXED_TIMESTAMP)
entry.create_system = 3
entry.external_attr = 0o100644 << 16
archive.writestr(entry, path.read_bytes())
temporary.replace(output)
finally:
temporary.unlink(missing_ok=True)

return 0


if __name__ == "__main__":
raise SystemExit(main())
44 changes: 23 additions & 21 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -419,35 +419,37 @@ int main(int argc, char* argv[]) {
});
#endif

try {
#ifdef BUILD_UI
// Default behavior with no subcommand: if the cwd looks like a project (has a
// config.yml), open the viewer and prompt for a ROM instead of printing help.
if (argc == 1 && std::filesystem::exists("config.yml")) {
const std::string rom = PromptForRom();
if (rom.empty()) {
std::cout << app.help() << std::endl;
return 0;
}
if (!std::filesystem::exists(rom)) {
std::cout << "ROM not found: " << rom << std::endl;
return 1;
}
Companion::Instance = new Companion(rom, ArchiveType::None, false, "", "");
// Default behavior with no subcommand: if the cwd looks like a project (has a
// config.yml), open the viewer and prompt for a ROM instead of printing help.
if (argc == 1 && std::filesystem::exists("config.yml")) {
const std::string rom = PromptForRom();
if (rom.empty()) {
std::cout << app.help() << std::endl;
return 0;
}
if (!std::filesystem::exists(rom)) {
std::cout << "ROM not found: " << rom << std::endl;
return 1;
}
Companion::Instance = new Companion(rom, ArchiveType::None, false, "", "");
#ifdef PM64_SUPPORT
PM64Audio::SetPreviewAssets(true);
PM64Audio::SetPreviewAssets(true);
#endif
std::atomic<size_t> assetCount{ 0 };
Companion::Instance->Init(ExportType::Binary, assetCount, false);
LaunchUI();
return 0;
}
std::atomic<size_t> assetCount{ 0 };
Companion::Instance->Init(ExportType::Binary, assetCount, false);
LaunchUI();
return 0;
}
#endif

try {
app.parse(argc, argv);
} catch (const CLI::ParseError& e) {
std::cout << app.help() << std::endl;
return app.exit(e);
} catch (const std::exception& e) {
std::cerr << "Torch failed: " << e.what() << std::endl;
return 1;
}

// No arguments --> display help.
Expand Down
135 changes: 122 additions & 13 deletions src/ui/backends/LusBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
#include "ui/backends/LusBackend.h"

#include <filesystem>
#include <memory>
#include <stdexcept>
#include <vector>
#include <zip.h>

#include "Companion.h"
#include "ui/Theme.h"
Expand Down Expand Up @@ -87,18 +90,116 @@ class ViewerControlDeck final : public Ship::ControlDeck {
class ViewerGui final : public Fast::Fast3dGui {
public:
using Fast::Fast3dGui::Fast3dGui;

// This LUS revision destroys its renderer before the base Window destructor
// shuts ImGui down. Release the platform/render backends while both the
// Context and renderer are still alive; the guarded overrides let the base
// destructor finish by destroying only the ImGui context.
void PrepareForShutdown() {
if (!mBackendsShutDown) {
Fast::Fast3dGui::ImGuiWMShutdown();
Fast::Fast3dGui::ImGuiBackendShutdown();
mBackendsShutDown = true;
}
}

void DrawGame() override {
}

protected:
void ImGuiWMShutdown() override {
if (!mBackendsShutDown) {
Fast::Fast3dGui::ImGuiWMShutdown();
}
}

void ImGuiBackendShutdown() override {
if (!mBackendsShutDown) {
Fast::Fast3dGui::ImGuiBackendShutdown();
}
}

private:
bool mBackendsShutDown = false;
};

class ViewerShutdownGuard final {
public:
explicit ViewerShutdownGuard(std::shared_ptr<ViewerGui> gui) : mGui(std::move(gui)) {
}

~ViewerShutdownGuard() {
mGui->PrepareForShutdown();
}

private:
std::shared_ptr<ViewerGui> mGui;
};

void ValidateShaderArchive(const std::string& path) {
int errorCode = 0;
std::unique_ptr<zip_t, decltype(&zip_discard)> archive(zip_open(path.c_str(), ZIP_RDONLY, &errorCode),
&zip_discard);
if (!archive) {
throw std::runtime_error("Torch viewer shader archive could not be opened: " + path);
}

constexpr const char* kRequiredShaders[] = {
"shaders/directx/default.shader.hlsl",
"shaders/metal/default.shader.metal",
"shaders/opengl/default.shader.glsl",
};
for (const char* shader : kRequiredShaders) {
if (zip_name_locate(archive.get(), shader, 0) < 0) {
throw std::runtime_error("Torch viewer shader archive is incomplete: " + path + " (missing " + shader +
")");
}
}
}

void AbandonPartiallyInitializedContext(std::shared_ptr<Ship::Context> context) {
// This pinned LUS Context destructor assumes a Window exists. A startup
// exception is terminal, so retain the incomplete Context until process exit
// and let main report the original error instead of crashing in teardown.
static auto* abandonedContexts = new std::vector<std::shared_ptr<Ship::Context>>();
abandonedContexts->push_back(std::move(context));
}

class ContextStartupGuard final {
public:
explicit ContextStartupGuard(std::shared_ptr<Ship::Context>& context) : mContext(context) {
}

~ContextStartupGuard() {
if (mArmed) {
AbandonPartiallyInitializedContext(std::move(mContext));
}
}

void Release() {
mArmed = false;
}

private:
std::shared_ptr<Ship::Context>& mContext;
bool mArmed = true;
};

class LusBackend final : public BaseBackend {
public:
void RunViewer(const std::shared_ptr<ViewManager>& views) override {
const auto shaderArchive = Ship::Context::GetPathRelativeToAppBundle("torch-lus-assets.o2r");
if (!std::filesystem::is_regular_file(shaderArchive)) {
throw std::runtime_error("Torch viewer shader archive is missing: " + shaderArchive);
}
ValidateShaderArchive(shaderArchive);

auto ctx = Ship::Context::CreateUninitializedInstance("Torch", "torch", "torch.cfg.json");
ctx->InitConfiguration();
ctx->InitConsoleVariables();
ctx->InitLogging();
ctx->InitControlDeck(std::make_shared<ViewerControlDeck>());
ContextStartupGuard contextStartupGuard(ctx);
if (!ctx->InitConfiguration() || !ctx->InitConsoleVariables() || !ctx->InitLogging() ||
!ctx->InitControlDeck(std::make_shared<ViewerControlDeck>())) {
throw std::runtime_error("Torch viewer could not initialize its LUS context");
}

// Mount the .o2r archives from the working directory so Fast3D can
// resolve the resources referenced by the previewed assets.
Expand All @@ -109,16 +210,15 @@ class LusBackend final : public BaseBackend {
archives.push_back(entry.path().string());
}
}
#ifdef TORCH_LUS_SHADER_DIR
// Mount the upstream LUS shaders last so they override any fork-modified
// shaders shipped inside the game archives (last archive added wins).
if (std::filesystem::exists(TORCH_LUS_SHADER_DIR)) {
archives.push_back(TORCH_LUS_SHADER_DIR);
archives.push_back(shaderArchive);
if (!ctx->InitResourceManager(archives, {}, 1)) {
throw std::runtime_error("Torch viewer could not initialize its resource manager");
}
if (!ctx->InitConsole() || !ctx->InitAudio(Ship::AudioSettings{})) {
throw std::runtime_error("Torch viewer could not initialize its LUS services");
}
#endif
ctx->InitResourceManager(archives, {}, 1);
ctx->InitConsole();
ctx->InitAudio(Ship::AudioSettings{});

// Fast3D binary resource factories (DisplayList/Vertex/Texture/Matrix/Light).
auto loader = ctx->GetResourceManager()->GetResourceLoader();
Expand All @@ -142,8 +242,14 @@ class LusBackend final : public BaseBackend {
// Context::GetWindow().
auto gui = std::make_shared<ViewerGui>(std::vector<std::shared_ptr<Ship::GuiWindow>>{});
auto window = std::make_shared<Fast::Fast3dWindow>(gui);
ctx->InitWindow(window);
ctx->InitEventSystem();
if (!ctx->InitWindow(window)) {
throw std::runtime_error("Torch viewer could not initialize its window");
}
ViewerShutdownGuard shutdownGuard(gui);
contextStartupGuard.Release();
if (!ctx->InitEventSystem()) {
throw std::runtime_error("Torch viewer could not initialize its event system");
}

window->GetGui()->AddGuiWindow(std::make_shared<ViewHostWindow>(views));

Expand All @@ -159,6 +265,9 @@ class LusBackend final : public BaseBackend {
static Gfx emptyDl[] = { gsSPEndDisplayList() };
while (window->IsRunning()) {
window->HandleEvents();
if (!window->IsRunning()) {
break;
}
PumpAudio();

// Render the previous frame's preview requests; the gui draw blits
Expand Down
Loading