diff --git a/cmake/PortableKit.cmake b/cmake/PortableKit.cmake index 90147fb..055353d 100644 --- a/cmake/PortableKit.cmake +++ b/cmake/PortableKit.cmake @@ -28,6 +28,7 @@ set(PORTABLEKIT_HOST_SOURCES host/corpus_library.cpp host/audio/atrac_decoder.cpp host/movie/avc_decoder.cpp + host/movie/replacement_movie.cpp host/movie/psmf_demuxer.cpp host/movie/psmf_header.cpp host/audio/audio_sink.cpp @@ -51,6 +52,7 @@ set(PORTABLEKIT_HOST_SOURCES host/kernel/rtc_time.cpp host/kernel/load_trace.cpp host/kernel/fast_loading.cpp + host/kernel/file_mods.cpp host/kernel/stall_watchdog.cpp host/kernel/load_detector.cpp host/kernel/iso_image.cpp @@ -101,9 +103,12 @@ set(PORTABLEKIT_RENDERER_SOURCES host/ui/font_menu.cpp host/input/motion.cpp host/input/steam_deck_imu.cpp + host/ui/i18n.cpp host/ui/input_script.cpp + host/ui/launcher.cpp host/ui/layer.cpp host/ui/menu.cpp + host/ui/save_chooser.cpp host/ui/save_screen.cpp host/ui/setup_screens.cpp host/ui/text_input.cpp @@ -276,6 +281,10 @@ function(portablekit_add_game target) if(MSVC) if(PSPRECOMP_GENERATED_OPT_LEVEL STREQUAL "0") set(generated_options "/Od;/bigobj") + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + # clang-cl: /Ob0 would turn off inlining; its optimizer copes + # with these units, as clang does on the other platforms. + set(generated_options "/O${PSPRECOMP_GENERATED_OPT_LEVEL};/bigobj") else() set(generated_options "/O${PSPRECOMP_GENERATED_OPT_LEVEL};/Ob0;/bigobj") endif() @@ -315,7 +324,8 @@ function(portablekit_add_game target) "kGeCheckVertexShader=GE_RAW_VERTICES,GE_CHECK_DECODE@${PORTABLEKIT_ROOT}/host/gpu/shaders/ge.vert" "kGeFragmentShader=${PORTABLEKIT_ROOT}/host/gpu/shaders/ge.frag" "kLevelVertexShader=${PORTABLEKIT_ROOT}/host/gpu/shaders/level.vert" - "kLevelFragmentShader=${PORTABLEKIT_ROOT}/host/gpu/shaders/level.frag") + "kLevelFragmentShader=${PORTABLEKIT_ROOT}/host/gpu/shaders/level.frag" + "kFxaaFragmentShader=${PORTABLEKIT_ROOT}/host/gpu/shaders/fxaa.frag") if(ANDROID) # Pre-rotation of the finished frame for a display turned sideways. list(APPEND shaders @@ -634,6 +644,8 @@ function(_portablekit_add_overlay host_target meta_path output_dir) target_compile_definitions(${target} PRIVATE PSPRECOMP_IMPORT_HOST_SYMBOLS=1) if(PSPRECOMP_GENERATED_OPT_LEVEL STREQUAL "0") target_compile_options(${target} PRIVATE /Od /bigobj) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(${target} PRIVATE /O${PSPRECOMP_GENERATED_OPT_LEVEL} /bigobj) else() target_compile_options(${target} PRIVATE /O${PSPRECOMP_GENERATED_OPT_LEVEL} /Ob0 /bigobj) endif() diff --git a/docs/PROFILE_GUIDE.md b/docs/PROFILE_GUIDE.md index fe7dcab..fce42a7 100644 --- a/docs/PROFILE_GUIDE.md +++ b/docs/PROFILE_GUIDE.md @@ -71,6 +71,12 @@ Two more are for a port that goes further than the PSP did, and both are null un `interpolation_thresholds` is for frame interpolation, which blends the game's frames and must not blend across a camera cut. The angles and fractions it uses to tell a cut from motion hold for any game; its distances are in the game's own world units, and the defaults were measured on Monster Hunter Portable 3rd. A game whose world is on a very different scale gives its own; one that does not, gets those defaults. +`adhoc_play` is false for a game with no multiplayer: the menu then has no Network page and no row about multiplayer. It is true by default, so a profile that says nothing keeps them. + +`launcher`, a `LauncherTheme`, puts a launcher before the game: a side bar with the game's logo and the menu's pages, a home page with a banner and quick settings, and a Play button. Its colours become the whole interface's, the in-game menu and the setup included. Its art is never the repository's: images the player puts in the data folder's `launcher/` (or an app's `Resources/launcher/`), else `PSP_GAME/PIC1.PNG` and `PSP_GAME/ICON0.PNG` read from their disc image. Null: the game starts at once, as before. The player can turn it off, `--play` skips it once and `--launcher` shows it once. Sparta's `host/sparta_profile.cpp` is the example. + +The interface speaks English and Brazilian Portuguese (`host/ui/i18n.hpp`): the widgets look up what they draw in a table, and the player picks the language under System. A profile's own text, such as a launcher's credits, is translated when the table has it and shown as written when it does not. + A driver writes into the game's memory, so check the code it relies on before it writes anything — Yakumo compares every instruction and constant it depends on at start-up, from `patch_loaded_image`, and stays out, saying why, if one differs. ## Generated code stays out of the repository diff --git a/host/app_paths.cpp b/host/app_paths.cpp index 8c8d1ce..c99ba8d 100644 --- a/host/app_paths.cpp +++ b/host/app_paths.cpp @@ -90,6 +90,11 @@ std::filesystem::path bundled_font_directory() { return shipped_directory("Resources", "fonts"); } +std::filesystem::path bundled_launcher_directory() { + if (!bundled_resource_directory().empty()) return bundled_resource_directory() / "launcher"; + return shipped_directory("Resources", "launcher"); +} + std::vector bundled_fonts() { std::vector fonts; const std::filesystem::path directory = bundled_font_directory(); diff --git a/host/app_paths.hpp b/host/app_paths.hpp index 8f224d7..6bd2a1e 100644 --- a/host/app_paths.hpp +++ b/host/app_paths.hpp @@ -23,6 +23,10 @@ namespace portablekit { // Contents/Resources/fonts in a macOS app bundle. [[nodiscard]] std::filesystem::path bundled_font_directory(); +// Where the images the launcher shows may ship (LauncherTheme): launcher/ next +// to the executable, or Contents/Resources/launcher in a macOS app bundle. +[[nodiscard]] std::filesystem::path bundled_launcher_directory(); + // Font files a release ships in fonts/ next to the executable, as fallbacks // after the system's own fonts. [[nodiscard]] std::vector bundled_fonts(); diff --git a/host/gpu/ge_state.cpp b/host/gpu/ge_state.cpp index 48af218..3732b1e 100644 --- a/host/gpu/ge_state.cpp +++ b/host/gpu/ge_state.cpp @@ -4,6 +4,8 @@ #include "perf/frame_stats.hpp" #include "psprecomp/common.hpp" +#include "triangle_indices.hpp" + #include #include #include @@ -43,6 +45,7 @@ enum Command : std::uint32_t { kAlphaBlendEnable = 0x21, kAlphaTestEnable = 0x22, kDepthTestEnable = 0x23, + kStencilTestEnable = 0x24, kBoneMatrixNumber = 0x2A, kBoneMatrixData = 0x2B, kWorldMatrixNumber = 0x3A, @@ -105,6 +108,8 @@ enum Command : std::uint32_t { kClutAddress = 0xB0, kClutAddressUpper = 0xB1, kTextureSize0 = 0xB8, + kTextureMapMode = 0xC0, + kTextureShadeLights = 0xC1, kTextureMode = 0xC2, kTextureFormat = 0xC3, kLoadClut = 0xC4, @@ -124,11 +129,19 @@ enum Command : std::uint32_t { kMinZ = 0xD6, kMaxZ = 0xD7, kAlphaTest = 0xDB, + // Read off God of War: Ghost of Sparta, which marks surfaces with + // replace under mask 0x3C and draws shadows where they equal 0x20 or 0x08: + // 0xDC 0x3C2001 is always, reference 0x20, mask 0x3C; 0xDC 0x202002 equal, + // reference 0x20, mask 0x20; 0xDD 0x020002 replace on stencil fail and on + // pass, keep on depth fail. + kStencilTest = 0xDC, + kStencilOp = 0xDD, kDepthTest = 0xDE, kBlendMode = 0xDF, kBlendFixedA = 0xE0, kBlendFixedB = 0xE1, kDepthWriteDisable = 0xE7, + kAlphaMask = 0xE9, // bits set are not written: the stencil's write mask // Block transfer registers, read off a trace of the quest reward screen, // which copies the 16-bit framebuffer at 0x04000000 (512 pixels a row) to // 0x093E7EF0 in main memory and then textures from there as 512x512 5650 @@ -632,6 +645,18 @@ void GeState::handle_command(const GuestMemory &memory, std::uint32_t command, s case kDepthTestEnable: depth_.test_enabled = (data & 1u) != 0u; break; case kDepthWriteDisable: depth_.write_enabled = (data & 1u) == 0u; break; case kDepthTest: depth_.function = data & 7u; break; + case kStencilTestEnable: stencil_.enabled = (data & 1u) != 0u; break; + case kStencilTest: + stencil_.function = data & 7u; + stencil_.reference = (data >> 8u) & 0xFFu; + stencil_.mask = (data >> 16u) & 0xFFu; + break; + case kStencilOp: + stencil_.fail = data & 7u; + stencil_.depth_fail = (data >> 8u) & 7u; + stencil_.pass = (data >> 16u) & 7u; + break; + case kAlphaMask: stencil_.write_mask = ~data & 0xFFu; break; case kMinZ: depth_.range_near = static_cast(data); break; case kMaxZ: depth_.range_far = static_cast(data); break; @@ -696,6 +721,14 @@ void GeState::handle_command(const GuestMemory &memory, std::uint32_t command, s texture_.wrap_s = data & 1u; texture_.wrap_t = (data >> 8u) & 1u; break; + case kTextureMapMode: + texture_.map_mode = data & 3u; + texture_.projection_source = (data >> 8u) & 3u; + break; + case kTextureShadeLights: + texture_.shade_light_u = data & 3u; + texture_.shade_light_v = (data >> 8u) & 3u; + break; case kTextureFunction: texture_.function = data & 7u; texture_.alpha_from_texture = ((data >> 8u) & 1u) != 0u; @@ -846,6 +879,8 @@ void GeState::handle_command(const GuestMemory &memory, std::uint32_t command, s << ") -> 0x" << std::hex << transfer.destination << std::dec << " (" << transfer.destination_x << "," << transfer.destination_y << " row " << transfer.destination_stride << ") " << transfer.width << "x" << transfer.height << " x" << transfer.bytes_per_pixel << " bytes\n"; + // The prims before it are drawn first. + flush_batch(); if (transfer_sink_) transfer_sink_(transfer); break; } @@ -941,27 +976,43 @@ struct IgnoredCommand { std::uint64_t count{}; std::uint32_t first_value{}; }; -std::map &ignored_commands() { - static std::map commands; +// Indexed by the command (8 bits) or 0x0E00 | a signal's behaviour: a busy +// frame ignores hundreds of commands, and a map lookup for each showed in +// profiles. +std::array &ignored_commands() { + static std::array commands{}; return commands; } } // namespace void GeState::trace_unhandled(std::uint32_t command, std::uint32_t data) { - IgnoredCommand &entry = ignored_commands()[command]; + IgnoredCommand &entry = ignored_commands()[command & 0xFFFu]; if (entry.count == 0u) entry.first_value = data; ++entry.count; static const bool trace = portablekit::env("TRACE_FB_TEXTURES") != nullptr; - if (!trace || entry.count > 4u) return; + if (!trace) return; + // Each value a command is given, the first sixteen: a state the game turns + // on once a scene (stencil, a logic op) comes long after its first values. + static std::array, 0x1000> values; + std::vector &seen = values[command & 0xFFFu]; + if (seen.size() >= 16u || std::find(seen.begin(), seen.end(), data) != seen.end()) return; + seen.push_back(data); std::cout << "[fbtex] unhandled GE command 0x" << std::hex << command << " data=0x" << data << std::dec << "\n"; } void GeState::report_ignored_commands() { - if (ignored_commands().empty()) return; - std::cout << "GE commands ignored: " << ignored_commands().size() << " distinct\n"; - for (const auto &[command, entry] : ignored_commands()) + const auto &commands = ignored_commands(); + const auto distinct = std::count_if(commands.begin(), commands.end(), [](const IgnoredCommand &entry) { + return entry.count != 0u; + }); + if (distinct == 0) return; + std::cout << "GE commands ignored: " << distinct << " distinct\n"; + for (std::uint32_t command = 0; command < commands.size(); ++command) { + const IgnoredCommand &entry = commands[command]; + if (entry.count == 0u) continue; std::cout << " 0x" << std::hex << command << std::dec << " x" << entry.count << " first=0x" << std::hex << entry.first_value << std::dec << "\n"; + } } void GeState::fill_draw_state(DrawCall &call) const { @@ -971,6 +1022,7 @@ void GeState::fill_draw_state(DrawCall &call) const { call.blend = blend_; call.depth = depth_; call.alpha_test = alpha_test_; + call.stencil = stencil_; call.viewport = viewport_; call.culling_enabled = culling_enabled_; call.cull_clockwise = cull_clockwise_; @@ -997,6 +1049,19 @@ void GeState::draw_primitive(const GuestMemory &memory, std::uint32_t data) { if (count == 0u || vertex_address_ == 0u) return; const std::uint32_t index_type = (vertex_type_ >> 11u) & 3u; + // A skipped frame draws nothing into buffers it redraws every frame: the + // pointers move on as the draw would have moved them, with nothing decoded. + if (skip_draw_ && skip_draw_(target_.color_address)) { + if (index_type != 0u && index_address_ != 0u) { + const std::uint32_t index_size = index_type == 1u ? 1u : index_type == 2u ? 2u : 4u; + if (memory.contains(index_address_, count * index_size)) index_address_ += count * index_size; + } else { + const std::uint32_t stride = decode_vertices(memory, vertex_address_, vertex_type_, 0u, call_.vertices); + if (stride != 0u && memory.contains(vertex_address_, static_cast(stride) * count)) + vertex_address_ += stride * count; + } + return; + } // One DrawCall is reused for every draw, so its vertex and index vectors // keep their capacity instead of being allocated and freed per draw. DrawCall &call = call_; @@ -1116,7 +1181,56 @@ void GeState::draw_primitive(const GuestMemory &memory, std::uint32_t data) { ++draw_count_; vertex_count_ += call.raw_vertices != nullptr ? call.raw_count : call.vertices.size(); - if (draw_sink_) draw_sink_(call); + send_draw(call); +} + +void GeState::send_draw(DrawCall &call) { + if (!draw_sink_) return; + static const bool off = portablekit::env("NO_DRAW_BATCH") != nullptr; + const bool triangles = call.primitive == PrimitiveType::Triangles || + call.primitive == PrimitiveType::TriangleStrip || + call.primitive == PrimitiveType::TriangleFan; + const bool sprites = call.primitive == PrimitiveType::Sprites && call.indices.empty(); + const bool batchable = !off && call.raw_vertices == nullptr && !call.vertices.empty() && (triangles || sprites); + if (batch_open_) { + if (batchable && state_serial_ == batch_serial_ && sprites == batch_sprites_ && + batch_.vertices.size() + call.vertices.size() <= 0xFFFFu) { + if (sprites) { + batch_.vertices.insert(batch_.vertices.end(), call.vertices.begin(), call.vertices.end()); + return; + } + if (!batch_indexed_) { + const std::size_t count = batch_.indices.empty() ? batch_.vertices.size() : batch_.indices.size(); + triangle_indices(batch_.primitive, count, batch_.indices, batch_.vertices.size(), batch_scratch_); + batch_.indices.swap(batch_scratch_); + batch_.primitive = PrimitiveType::Triangles; + batch_indexed_ = true; + } + const std::size_t count = call.indices.empty() ? call.vertices.size() : call.indices.size(); + const auto base = static_cast(batch_.vertices.size()); + append_triangle_indices(call.primitive, count, call.indices, call.vertices.size(), base, batch_.indices); + batch_.vertices.insert(batch_.vertices.end(), call.vertices.begin(), call.vertices.end()); + return; + } + flush_batch(); + } + if (!batchable) { + draw_sink_(call); + return; + } + // The prim opens a batch: its call becomes the batch, and the batch's old + // buffers go back to be decoded into. + std::swap(batch_, call); + batch_open_ = true; + batch_indexed_ = false; + batch_sprites_ = sprites; + batch_serial_ = state_serial_; +} + +void GeState::flush_batch() { + if (!batch_open_) return; + batch_open_ = false; + if (draw_sink_) draw_sink_(batch_); } namespace { @@ -1359,7 +1473,8 @@ std::uint32_t GeState::execute(const GuestMemory &memory, std::uint32_t pc, std: // _TRACE_GE_LIST=N[:M]: every command word of display list runs // N to N+M-1 (default one), counted from the first, to read what a - // screen is built from when it draws nothing sensible. + // screen is built from when it draws nothing sensible. Each run prints + // its first 20000 words; _TRACE_GE_LIST_WORDS changes that. static const auto list_trace = []() -> std::pair { const char *text = portablekit::env("TRACE_GE_LIST"); if (text == nullptr) return {~0ull, 0ull}; @@ -1368,16 +1483,44 @@ std::uint32_t GeState::execute(const GuestMemory &memory, std::uint32_t pc, std: const std::uint64_t count = end != nullptr && *end == ':' ? std::strtoull(end + 1, nullptr, 0) : 1ull; return {first, count}; }(); + static const std::uint64_t list_trace_words = [] { + const char *text = portablekit::env("TRACE_GE_LIST_WORDS"); + return text != nullptr ? std::strtoull(text, nullptr, 0) : 20000ull; + }(); static std::uint64_t runs = 0; const std::uint64_t run = runs++; const bool dump = run >= list_trace.first && run - list_trace.first < list_trace.second; if (dump) std::cerr << "[ge-list] run " << run << " from " << psprecomp::hex32(pc) << "\n"; + // The list's words are read through a host pointer to a window of it, so + // a command costs no range check and no call while the list runs on in + // one region; a jump or call outside the window looks it up again. + // _NO_LIST_WINDOW reads every word through GuestMemory instead. + static const bool no_window = portablekit::env("NO_LIST_WINDOW") != nullptr; + constexpr std::uint32_t kWindowBytes = 1024u; + const std::uint8_t *window = nullptr; + std::uint32_t window_base = 0u; + std::uint32_t window_bytes = 0u; + for (std::uint32_t steps = 0; steps < 2'000'000u; ++steps) { - if (stall != 0u && pc == stall) return pc; - if (!memory.contains(pc, 4u)) return pc; - const std::uint32_t word = memory.load32(pc); - if (dump && steps < 20000u) + if (stall != 0u && pc == stall) { + flush_batch(); + return pc; + } + std::uint32_t word; + if (pc - window_base < window_bytes) { + std::memcpy(&word, window + (pc - window_base), sizeof(word)); + } else { + if (!memory.contains(pc, 4u)) { + flush_batch(); + return pc; + } + window = no_window ? nullptr : memory.raw_pointer(pc, kWindowBytes); + window_base = pc; + window_bytes = window != nullptr ? kWindowBytes : 0u; + word = memory.load32(pc); + } + if (dump && steps < list_trace_words) std::cerr << "[ge-list] " << psprecomp::hex32(pc) << " " << psprecomp::hex32(word) << "\n"; const std::uint32_t command = word >> 24u; const std::uint32_t data = word & 0x00FFFFFFu; @@ -1411,6 +1554,7 @@ std::uint32_t GeState::execute(const GuestMemory &memory, std::uint32_t pc, std: continue; case kEnd: finished = true; + flush_batch(); return pc; case kFinish: if (signal_sink_) signal_sink_(0x10000u | (data & 0xFFFFu), pc); @@ -1460,13 +1604,22 @@ std::uint32_t GeState::execute(const GuestMemory &memory, std::uint32_t pc, std: continue; case kBezier: case kSpline: + flush_batch(); draw_patch(memory, command, data); continue; default: + // Pointers and the bone matrices (applied as the vertices are + // decoded) leave a batch open; anything else may change how the + // next prim draws. + if (command != kVertexAddress && command != kIndexAddress && command != kBase && + command != kOffsetAddress && command != kBoneMatrixNumber && command != kBoneMatrixData && + command != 0x00u) + ++state_serial_; handle_command(memory, command, data); continue; } } + flush_batch(); return pc; } diff --git a/host/gpu/ge_state.hpp b/host/gpu/ge_state.hpp index a187ab4..0475584 100644 --- a/host/gpu/ge_state.hpp +++ b/host/gpu/ge_state.hpp @@ -75,6 +75,13 @@ struct TextureState { float scale_v{1.0f}; float offset_u{}; float offset_v{}; + // Where transformed vertices get their texture coordinates (TMAP, 0xC0): + // 0 their own UV, 1 the texture matrix applied to the projection source, + // 2 environment (shade) mapping, from the lights TEXSHADELS (0xC1) names. + std::uint32_t map_mode{}; + std::uint32_t projection_source{}; // 0 position, 1 UV, 2 normalized normal, 3 normal + std::uint32_t shade_light_u{}; + std::uint32_t shade_light_v{}; }; struct RenderTarget { @@ -109,6 +116,23 @@ struct AlphaTestState { std::uint32_t mask{0xFFu}; }; +// The stencil test. The PSP keeps the stencil in the framebuffer's alpha; +// the renderer keeps it in a stencil buffer beside the depth buffer. +// Functions as the GE numbers them (0 never, 1 always, 2 equal, 3 not equal, +// 4 less, 5 less or equal, 6 greater, 7 greater or equal: the reference +// compared with the buffer's value), operations too (0 keep, 1 zero, +// 2 replace, 3 invert, 4 increment, 5 decrement). +struct StencilState { + bool enabled{}; + std::uint32_t function{1u}; + std::uint32_t reference{}; + std::uint32_t mask{0xFFu}; + std::uint32_t fail{}; // when the stencil test fails + std::uint32_t depth_fail{}; // when it passes and the depth test fails + std::uint32_t pass{}; // when both pass + std::uint32_t write_mask{0xFFu}; // bits that may change: the GE's alpha mask register inverted +}; + struct ViewportState { float x_scale{}, y_scale{}, z_scale{}; float x_offset{}, y_offset{}, z_offset{}; @@ -166,6 +190,7 @@ struct DrawCall { BlendState blend; DepthState depth; AlphaTestState alpha_test; + StencilState stencil; ViewportState viewport; bool culling_enabled{}; bool cull_clockwise{}; @@ -242,6 +267,10 @@ class GeState { using TransferSink = std::function; void set_draw_sink(DrawSink sink) { draw_sink_ = std::move(sink); } + // Frame skipping: asked with a draw's colour buffer before its vertices + // are decoded; true drops the draw (the pointers still advance past it). + using SkipDraw = std::function; + void set_skip_draw(SkipDraw skip) { skip_draw_ = std::move(skip); } void set_signal_sink(SignalSink sink) { signal_sink_ = std::move(sink); } // Block transfers are carried out by the sink, which can write guest memory. void set_transfer_sink(TransferSink sink) { transfer_sink_ = std::move(sink); } @@ -291,6 +320,7 @@ class GeState { BlendState blend_{}; DepthState depth_{}; AlphaTestState alpha_test_{}; + StencilState stencil_{}; ViewportState viewport_{}; bool culling_enabled_{}; bool cull_clockwise_{}; @@ -323,7 +353,24 @@ class GeState { std::vector call_stack_; DrawCall call_; // reused by draw_primitive for every draw + // Prims drawn in a row with nothing changed between them but the vertex + // and index pointers go to the sink as one draw: a game drawing a mesh as + // hundreds of small prims (God of War: Ghost of Sparta makes some 6500 + // a frame, nine vertices each) cost the renderer's whole per-draw work + // each time. Triangles, strips and fans join as one indexed triangle list, + // in the order the renderer expands them in; unindexed sprites join as + // they are. _NO_DRAW_BATCH sends every prim on its own. + DrawCall batch_; + bool batch_open_{}; + bool batch_indexed_{}; // made into an indexed triangle list + bool batch_sprites_{}; + std::uint64_t batch_serial_{}; + std::uint64_t state_serial_{}; // moves with every command that changes how a prim draws + std::vector batch_scratch_; + void send_draw(DrawCall &call); + void flush_batch(); DrawSink draw_sink_; + SkipDraw skip_draw_; SignalSink signal_sink_; TransferSink transfer_sink_; std::uint64_t draw_count_{}; diff --git a/host/gpu/shaders/fxaa.frag b/host/gpu/shaders/fxaa.frag new file mode 100644 index 0000000..e9230be --- /dev/null +++ b/host/gpu/shaders/fxaa.frag @@ -0,0 +1,127 @@ +#version 450 + +// Antialiasing (video.antialiasing = fxaa): fast approximate antialiasing of +// the finished picture, at the internal resolution, before it is scaled to the +// window. The vertex shader is the level horizon's: one triangle over the +// viewport, which is the whole picture. +// +// For each pixel: if the contrast around it is low, it is left as it is. +// Otherwise the edge through it is found (horizontal or vertical, from the +// second differences of luma), followed both ways along the picture until the +// luma no longer matches the edge, and the pixel is resampled moved towards +// the other side of the edge by how near it lies to the closer end, so a +// staircase becomes a slope. Thin features shorter than a pixel get a blend +// with their neighbours instead. + +layout(set = 0, binding = 0) uniform sampler2D frame; + +layout(push_constant) uniform Pass { + // The size of one texel of the picture, 0 to 1. + vec2 texel; +} pass; + +layout(location = 0) in vec2 picture; +layout(location = 0) out vec4 color; + +// Contrast below which a pixel is no edge: relative to the brightest +// neighbour, and in dark areas an absolute floor. +const float kContrast = 0.125; +const float kContrastFloor = 0.0312; +// How much of the blend thin features take, 0 to 1. +const float kSubpixel = 0.75; +// Steps along an edge, in texels: fine near the pixel, longer further away. +const int kSteps = 10; +const float kStep[kSteps] = float[](1.0, 1.0, 1.0, 1.0, 1.0, 1.5, 2.0, 2.0, 4.0, 8.0); + +float luma(vec3 rgb) { return dot(rgb, vec3(0.299, 0.587, 0.114)); } +float luma_at(vec2 uv) { return luma(textureLod(frame, uv, 0.0).rgb); } +float luma_near(vec2 uv, vec2 offset) { return luma_at(uv + offset * pass.texel); } + +void main() { + vec2 uv = picture; + vec4 centre = textureLod(frame, uv, 0.0); + float m = luma(centre.rgb); + float n = luma_near(uv, vec2(0, -1)); + float s = luma_near(uv, vec2(0, 1)); + float e = luma_near(uv, vec2(1, 0)); + float w = luma_near(uv, vec2(-1, 0)); + float low = min(m, min(min(n, s), min(e, w))); + float high = max(m, max(max(n, s), max(e, w))); + float range = high - low; + if (range < max(kContrastFloor, high * kContrast)) { + color = centre; + return; + } + float nw = luma_near(uv, vec2(-1, -1)); + float ne = luma_near(uv, vec2(1, -1)); + float sw = luma_near(uv, vec2(-1, 1)); + float se = luma_near(uv, vec2(1, 1)); + + // Thin features: how far the pixel stands out from the average around it. + float average = (2.0 * (n + s + e + w) + nw + ne + sw + se) / 12.0; + float subpixel = smoothstep(0.0, 1.0, clamp(abs(average - m) / range, 0.0, 1.0)); + subpixel = subpixel * subpixel * kSubpixel; + + // Luma changing from row to row is a horizontal edge; from column to + // column, a vertical one. + float across_rows = abs(nw + sw - 2.0 * w) + 2.0 * abs(n + s - 2.0 * m) + abs(ne + se - 2.0 * e); + float across_columns = abs(nw + ne - 2.0 * n) + 2.0 * abs(w + e - 2.0 * m) + abs(sw + se - 2.0 * s); + bool horizontal = across_rows >= across_columns; + + // Which side of the pixel the edge lies on: where the luma changes most. + float before = horizontal ? n : w; + float after = horizontal ? s : e; + float change_before = abs(before - m); + float change_after = abs(after - m); + bool towards_before = change_before >= change_after; + float gradient = max(change_before, change_after); + float across = horizontal ? pass.texel.y : pass.texel.x; + float edge_luma; + if (towards_before) { + across = -across; + edge_luma = 0.5 * (before + m); + } else { + edge_luma = 0.5 * (after + m); + } + + // Follow the edge, half a texel across, both ways until either end. + vec2 on_edge = uv; + if (horizontal) on_edge.y += 0.5 * across; + else on_edge.x += 0.5 * across; + vec2 along = horizontal ? vec2(pass.texel.x, 0.0) : vec2(0.0, pass.texel.y); + float threshold = 0.25 * gradient; + vec2 forward = on_edge + along * kStep[0]; + vec2 backward = on_edge - along * kStep[0]; + float forward_change = luma_at(forward) - edge_luma; + float backward_change = luma_at(backward) - edge_luma; + bool forward_done = abs(forward_change) >= threshold; + bool backward_done = abs(backward_change) >= threshold; + for (int i = 1; i < kSteps && !(forward_done && backward_done); ++i) { + if (!forward_done) { + forward += along * kStep[i]; + forward_change = luma_at(forward) - edge_luma; + forward_done = abs(forward_change) >= threshold; + } + if (!backward_done) { + backward -= along * kStep[i]; + backward_change = luma_at(backward) - edge_luma; + backward_done = abs(backward_change) >= threshold; + } + } + float forward_distance = horizontal ? forward.x - uv.x : forward.y - uv.y; + float backward_distance = horizontal ? uv.x - backward.x : uv.y - backward.y; + bool forward_nearer = forward_distance <= backward_distance; + float distance = min(forward_distance, backward_distance); + float span = forward_distance + backward_distance; + // Moved only when the nearer end crosses the edge the other way from + // this pixel: that is where the staircase steps. + float end_change = forward_nearer ? forward_change : backward_change; + bool pixel_darker = m < edge_luma; + float edge_offset = ((end_change < 0.0) != pixel_darker) ? 0.5 - distance / span : 0.0; + float offset = max(edge_offset, subpixel); + + vec2 moved = uv; + if (horizontal) moved.y += offset * across; + else moved.x += offset * across; + color = vec4(textureLod(frame, moved, 0.0).rgb, centre.a); +} diff --git a/host/gpu/shaders/ge.frag b/host/gpu/shaders/ge.frag index f180930..fa8ad66 100644 --- a/host/gpu/shaders/ge.frag +++ b/host/gpu/shaders/ge.frag @@ -1,6 +1,6 @@ #version 450 -layout(location = 0) in vec2 frag_texcoord; +layout(location = 0) in vec3 frag_texcoord; // u, v, q layout(location = 1) in vec4 frag_color; layout(location = 2) in vec3 frag_specular; layout(location = 3) in float frag_fog; @@ -32,7 +32,9 @@ layout(set = 1, binding = 0) uniform Environment { void main() { vec4 color = frag_color; if (push.texture_params.x > 0.5) { - vec2 uv = clamp(frag_texcoord, frag_uv_rect.xy, frag_uv_rect.zw); + // Projection mapping divides by q here, per pixel; q is 1 otherwise. + float q = abs(frag_texcoord.z) > 1e-7 ? frag_texcoord.z : 1e-7; + vec2 uv = clamp(frag_texcoord.xy / q, frag_uv_rect.xy, frag_uv_rect.zw); // The GE's clamp wrap mode, per axis (texture_params.x bits 1 and 2): // the sampler repeats, so the coordinate stays between the centres of // the edge texels, which is what clamping to the edge samples. diff --git a/host/gpu/shaders/ge.vert b/host/gpu/shaders/ge.vert index 476ebf6..63a55f3 100644 --- a/host/gpu/shaders/ge.vert +++ b/host/gpu/shaders/ge.vert @@ -155,7 +155,9 @@ void decode_raw_vertex() { } #endif -layout(location = 0) out vec2 frag_texcoord; +// u, v and q: the GE's projection mapping divides by q per pixel; every +// other draw has q = 1. +layout(location = 0) out vec3 frag_texcoord; layout(location = 1) out vec4 frag_color; layout(location = 2) out vec3 frag_specular; layout(location = 3) out float frag_fog; @@ -197,8 +199,35 @@ layout(set = 1, binding = 1) uniform Object { vec4 material_ambient; // rgba vec4 material_diffuse; // rgb; w: 1 keeps specular apart vec4 material_specular; // rgb; w: reverse normals + mat4 texture_matrix; // TMAP mode 1 + vec4 texgen; // x: TMAP mode, y: projection source, z/w: the lights shade mapping uses for u/v } object; +// Texture coordinates the GE makes instead of taking the vertex's (TMAP modes +// 1 and 2), in [0, 1] texture space; the UV scale and offset do not apply. +// Mode 1 multiplies the projection source (the model-space position, the +// UV, or the normal) by the texture matrix and divides by the third +// component; mode 2 maps the world-space normal against two lights' directions. +vec3 generate_texcoord() { + int mode = int(object.texgen.x + 0.5); + vec3 normal = dot(in_normal, in_normal) > 0.0 ? in_normal : vec3(0.0, 0.0, 1.0); + if (mode == 1) { + int source = int(object.texgen.y + 0.5); + vec3 from = source == 0 ? in_position.xyz + : source == 1 ? vec3(in_texcoord, 0.0) + : source == 2 ? normalize(normal) + : in_normal; + return (object.texture_matrix * vec4(from, 1.0)).xyz; + } + vec3 world_normal = mat3(object.world) * normal; + world_normal = dot(world_normal, world_normal) > 0.0 ? normalize(world_normal) : vec3(0.0, 0.0, 1.0); + vec3 light_u = lighting.light_position[int(object.texgen.z + 0.5)].xyz; + vec3 light_v = lighting.light_position[int(object.texgen.w + 0.5)].xyz; + light_u = dot(light_u, light_u) > 0.0 ? normalize(light_u) : vec3(0.0, 0.0, 1.0); + light_v = dot(light_v, light_v) > 0.0 ? normalize(light_v) : vec3(0.0, 0.0, 1.0); + return vec3((1.0 + dot(light_u, world_normal)) * 0.5, (1.0 + dot(light_v, world_normal)) * 0.5, 1.0); +} + // The GE's per-vertex lighting, evaluated in world space: emissive, plus the // global ambient light times the material ambient, plus for each enabled light // its ambient, diffuse and specular terms, scaled by distance attenuation and @@ -269,7 +298,7 @@ void main() { #ifdef GE_RAW_VERTICES decode_raw_vertex(); #endif - frag_texcoord = in_texcoord * push.uv_transform.xy + push.uv_transform.zw; + frag_texcoord = vec3(in_texcoord * push.uv_transform.xy + push.uv_transform.zw, 1.0); frag_color = in_color; frag_specular = vec3(0.0); frag_fog = 1.0; @@ -284,6 +313,7 @@ void main() { } else { int enables = int(push.viewport.w + 0.5); if ((enables & 2) != 0) light_vertex(frag_color, frag_specular); + if ((enables & 4) != 0) frag_texcoord = generate_texcoord(); // Fog runs linearly from 1 (clear) to 0 (fogged) with view-space z, // which is negative in front of the camera: (z + end) * scale. if ((enables & 1) != 0) diff --git a/host/gpu/texture_pack.cpp b/host/gpu/texture_pack.cpp index 28628eb..899e37a 100644 --- a/host/gpu/texture_pack.cpp +++ b/host/gpu/texture_pack.cpp @@ -328,7 +328,9 @@ std::unique_ptr TexturePack::parse(const std::filesystem::path &dir const std::size_t equals = line.find('='); if (equals == std::string::npos) continue; pack->games_.push_back(trim(line.substr(0, equals))); - if (pack->games_.back() != game_id) continue; + if (pack->games_.back() != game_id && + !(game_id == portablekit::game().disc_id && portablekit::texture_pack_fits(pack->games_.back()))) + continue; const std::string name = trim(line.substr(equals + 1u)); if (name.empty() || name == "true" || name == "textures.ini") continue; if (name.find("..") != std::string::npos) { diff --git a/host/gpu/texture_pack_import.cpp b/host/gpu/texture_pack_import.cpp index b105f1c..5bf8b72 100644 --- a/host/gpu/texture_pack_import.cpp +++ b/host/gpu/texture_pack_import.cpp @@ -70,9 +70,25 @@ bool holds_pack(const fs::path &folder) { return !folder.empty() && (is_file(folder / "textures.ini") || is_file(folder / "textures.zip")); } +// Whether a pack made for `name` fits `game_id`: the same id, or, for the +// game this port plays, a release its profile says has the same textures. +bool fits(const std::string &name, const std::string &game_id) { + if (lower(name) == lower(game_id)) return true; + return lower(game_id) == lower(portablekit::game().disc_id) && portablekit::texture_pack_fits(name); +} + bool lists(const std::vector &games, const std::string &game_id) { - const std::string wanted = lower(game_id); - return std::any_of(games.begin(), games.end(), [&](const std::string &game) { return lower(game) == wanted; }); + return std::any_of(games.begin(), games.end(), [&](const std::string &game) { return fits(game, game_id); }); +} + +// The disc ids a pack folder for `game_id` may be named: its own first, then +// the releases whose packs fit it. +std::vector pack_names(const std::string &game_id) { + std::vector names{game_id}; + if (lower(game_id) == lower(portablekit::game().disc_id)) + for (const char *release : portablekit::game().texture_pack_releases) + if (release != nullptr) names.emplace_back(release); + return names; } // Files a copy leaves out: the file managers' own (.DS_Store, Thumbs.db, @@ -155,15 +171,18 @@ fs::path find_pack(const fs::path &chosen, const std::string &game_id, std::stri fs::path folder; const char *layout; }; - const Place exact[] = { - {child(textures, game_id), "Found in its textures folder."}, - {child(chosen, game_id), "Found in the folder."}, - {child(psp_textures, game_id), "Found in PPSSPP's PSP/TEXTURES folder."}, - }; - for (const Place &place : exact) { - if (holds_pack(place.folder)) { - layout = place.layout; - return place.folder; + for (const std::string &name : pack_names(game_id)) { + const Place exact[] = { + {child(textures, name), "Found in its textures folder."}, + {child(chosen, name), "Found in the folder."}, + {child(psp_textures, name), "Found in PPSSPP's PSP/TEXTURES folder."}, + }; + for (const Place &place : exact) { + if (holds_pack(place.folder)) { + layout = place.layout; + if (lower(name) != lower(game_id)) made_for = name_of(place.folder); + return place.folder; + } } } // A pack under another name whose textures.ini says it covers this game. @@ -208,6 +227,12 @@ TexturePackLocation texture_pack_location(const fs::path &textures_root, const s } if (!in_place.empty()) return {fs::path(std::u8string(in_place.begin(), in_place.end())), TexturePackLocation::Source::InPlace}; + // A pack copied in by hand may keep the name of the release it was made + // for; the game's own folder still comes first. + if (!holds_pack(textures_root / game_id)) + for (const std::string &name : pack_names(game_id)) + if (const fs::path folder = child(textures_root, name); holds_pack(folder)) + return {folder, TexturePackLocation::Source::Installed}; return {textures_root / game_id, TexturePackLocation::Source::Installed}; } @@ -232,7 +257,7 @@ TexturePackCheck check_texture_pack(const fs::path &chosen, const std::string &g // covers this game too. const std::string folder_name = name_of(check.folder); const std::vector games = texture_pack_games(check.folder / "textures.ini"); - if (lower(folder_name) != lower(game_id) && !lists(games, game_id)) { + if (!fits(folder_name, game_id) && !lists(games, game_id)) { if (looks_like_game_id(folder_name)) { check.problem = "This pack is for " + folder_name + ", and its textures.ini does not list " + game_id + " under [games], so its textures would not match this release."; @@ -244,6 +269,7 @@ TexturePackCheck check_texture_pack(const fs::path &chosen, const std::string &g return check; } } else if (lower(folder_name) != lower(game_id) && looks_like_game_id(folder_name)) { + // Named for another release that fits this one, or listing this one. check.made_for = folder_name; } @@ -308,6 +334,23 @@ fs::path texture_pack_backup_directory(const fs::path &textures_root, std::chron return unused_path(textures_root / ".backup", timestamp_for_path(time)); } +fs::path new_texture_pack_staging(const fs::path &textures_root) { + // Copies that never finished (the game was closed or crashed during one) + // are this code's own folders; nothing else is named like them. + std::error_code ec; + for (fs::directory_iterator it(textures_root, ec); !ec && it != fs::directory_iterator(); it.increment(ec)) { + if (name_of(it->path()).rfind(kStagingPrefix, 0) != 0 || !is_folder(it->path())) continue; + std::error_code remove_error; + fs::remove_all(it->path(), remove_error); + std::cout << "[texpack] removed an unfinished copy, " << utf8(it->path()) << "\n"; + } + return unused_path(textures_root, kStagingPrefix + timestamp_for_path(std::chrono::system_clock::now())); +} + +bool texture_pack_file_skipped(const std::string &name) { return skipped(name); } + +std::vector texture_pack_folder_names(const std::string &game_id) { return pack_names(game_id); } + TexturePackCopy::~TexturePackCopy() { cancel(); join(); @@ -325,14 +368,7 @@ void TexturePackCopy::start(const TexturePackCheck &check, const fs::path &textu } // Copies that never finished (the game was closed or crashed during one) // are this code's own folders; nothing else is named like them. - std::error_code ec; - for (fs::directory_iterator it(textures_root, ec); !ec && it != fs::directory_iterator(); it.increment(ec)) { - if (name_of(it->path()).rfind(kStagingPrefix, 0) != 0 || !is_folder(it->path())) continue; - std::error_code remove_error; - fs::remove_all(it->path(), remove_error); - std::cout << "[texpack] removed an unfinished copy, " << utf8(it->path()) << "\n"; - } - staging_ = unused_path(textures_root, kStagingPrefix + timestamp_for_path(std::chrono::system_clock::now())); + staging_ = new_texture_pack_staging(textures_root); state_ = State::Copying; thread_ = std::thread([this, source = check.folder] { run(source); }); } diff --git a/host/gpu/texture_pack_import.hpp b/host/gpu/texture_pack_import.hpp index bd59b27..2182a0b 100644 --- a/host/gpu/texture_pack_import.hpp +++ b/host/gpu/texture_pack_import.hpp @@ -87,6 +87,19 @@ inline constexpr std::uint64_t kTexturePackSpaceMargin = 64ull * 1024u * 1024u; [[nodiscard]] std::filesystem::path texture_pack_backup_directory(const std::filesystem::path &textures_root, std::chrono::system_clock::time_point time); +// A new staging folder's path in `textures_root`, +// textures/.incomplete-_