Merge pull request #485 from RomainROCH/codex/fix-jpeg-formats
fix(jpegenc): honor chroma sampling and encode true grayscale JPEGs
This commit is contained in:
commit
fbcba205de
|
|
@ -28,3 +28,6 @@
|
|||
[submodule "3rdparty/stb"]
|
||||
path = 3rdparty/stb
|
||||
url = https://github.com/nothings/stb.git
|
||||
[submodule "3rdparty/libjpeg-turbo"]
|
||||
path = 3rdparty/libjpeg-turbo
|
||||
url = https://github.com/libjpeg-turbo/libjpeg-turbo.git
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit c85e6b905bf237038faa936dab160ebfc5da0344
|
||||
|
|
@ -3,10 +3,56 @@ function(add_jpeg_decoder target runtimeTarget)
|
|||
message(FATAL_ERROR "JPEG decoder requires an existing runtime target: ${runtimeTarget}")
|
||||
endif()
|
||||
|
||||
include(ExternalProject)
|
||||
if(NOT TARGET jpeg_turbo_external)
|
||||
set(jpeg_turbo_prefix "${CMAKE_BINARY_DIR}/libjpeg-turbo")
|
||||
if(WIN32)
|
||||
if(MSVC)
|
||||
set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/turbojpeg-static.lib")
|
||||
else()
|
||||
set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/libturbojpeg.a")
|
||||
endif()
|
||||
else()
|
||||
set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/libturbojpeg.a")
|
||||
endif()
|
||||
file(MAKE_DIRECTORY "${jpeg_turbo_prefix}/include")
|
||||
ExternalProject_Add(jpeg_turbo_external
|
||||
SOURCE_DIR "${PROJECT_SOURCE_DIR}/3rdparty/libjpeg-turbo"
|
||||
BINARY_DIR "${CMAKE_BINARY_DIR}/libjpeg-turbo-build"
|
||||
INSTALL_DIR "${jpeg_turbo_prefix}"
|
||||
CMAKE_ARGS
|
||||
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
|
||||
-DCMAKE_INSTALL_PREFIX=${jpeg_turbo_prefix}
|
||||
-DBUILD_SHARED_LIBS=OFF
|
||||
-DENABLE_STATIC=ON
|
||||
-DENABLE_SHARED=OFF
|
||||
-DWITH_SIMD=OFF
|
||||
-DWITH_TURBOJPEG=ON
|
||||
-DWITH_TOOLS=OFF
|
||||
-DWITH_TESTS=OFF
|
||||
-DCMAKE_INSTALL_LIBDIR=lib
|
||||
-DCMAKE_C_VISIBILITY_PRESET=hidden
|
||||
-DCMAKE_VISIBILITY_INLINES_HIDDEN=ON
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
|
||||
-DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER}
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER}
|
||||
BUILD_BYPRODUCTS
|
||||
"${jpeg_turbo_library}"
|
||||
)
|
||||
add_library(jpeg_turbo STATIC IMPORTED GLOBAL)
|
||||
set_target_properties(jpeg_turbo PROPERTIES
|
||||
IMPORTED_LOCATION "${jpeg_turbo_library}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${PROJECT_SOURCE_DIR}/3rdparty/libjpeg-turbo/src;${jpeg_turbo_prefix}/include"
|
||||
)
|
||||
add_dependencies(jpeg_turbo jpeg_turbo_external)
|
||||
endif()
|
||||
|
||||
add_library(${target} STATIC ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/src/Jpeg.cpp)
|
||||
target_include_directories(${target} PUBLIC ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/include)
|
||||
target_include_directories(${target} SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/3rdparty/stb)
|
||||
target_link_libraries(${target} PRIVATE ${runtimeTarget})
|
||||
target_link_libraries(${target} PRIVATE ${runtimeTarget} jpeg_turbo)
|
||||
set_target_properties(${target} PROPERTIES
|
||||
CXX_EXTENSIONS OFF
|
||||
CXX_VISIBILITY_PRESET hidden
|
||||
|
|
|
|||
|
|
@ -21,8 +21,14 @@ struct Header {
|
|||
std::uint32_t channels;
|
||||
};
|
||||
|
||||
enum class Sampling {
|
||||
Yuv444,
|
||||
Yuv422,
|
||||
Yuv420,
|
||||
};
|
||||
|
||||
std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint32_t width, std::uint32_t height,
|
||||
std::uint32_t channels, int quality);
|
||||
std::uint32_t channels, int quality, Sampling sampling = Sampling::Yuv444);
|
||||
|
||||
std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,12 @@
|
|||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
|
||||
#define STB_IMAGE_WRITE_STATIC
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#define STBI_WRITE_NO_STDIO
|
||||
#include "stb_image_write.h"
|
||||
|
||||
#define STB_IMAGE_STATIC
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STBI_ONLY_JPEG
|
||||
#define STBI_NO_STDIO
|
||||
#include "stb_image.h"
|
||||
#include "turbojpeg.h"
|
||||
|
||||
namespace Decoder::Jpeg {
|
||||
|
||||
|
|
@ -21,16 +17,39 @@ namespace {
|
|||
|
||||
constexpr std::uint32_t MAX_DIMENSION = 0xFFFF;
|
||||
|
||||
void appendBytes(void* context, void* data, int size) {
|
||||
auto* output = static_cast<std::vector<std::uint8_t>*>(context);
|
||||
const auto* bytes = static_cast<const std::uint8_t*>(data);
|
||||
output->insert(output->end(), bytes, bytes + size);
|
||||
class Handle {
|
||||
public:
|
||||
Handle() : handle(tj3Init(TJINIT_COMPRESS)) {
|
||||
if (!handle) throw std::runtime_error("Jpeg::Encode: encoder initialization failed");
|
||||
}
|
||||
~Handle() { tj3Destroy(handle); }
|
||||
Handle(const Handle&) = delete;
|
||||
Handle& operator=(const Handle&) = delete;
|
||||
operator tjhandle() const { return handle; }
|
||||
|
||||
private:
|
||||
tjhandle handle;
|
||||
};
|
||||
|
||||
class Buffer {
|
||||
public:
|
||||
~Buffer() { tj3Free(data); }
|
||||
unsigned char* data = nullptr;
|
||||
};
|
||||
|
||||
int toSubsampling(Sampling sampling) {
|
||||
switch (sampling) {
|
||||
case Sampling::Yuv444: return TJSAMP_444;
|
||||
case Sampling::Yuv422: return TJSAMP_422;
|
||||
case Sampling::Yuv420: return TJSAMP_420;
|
||||
}
|
||||
throw std::invalid_argument("Jpeg::Encode: invalid sampling");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint32_t width, std::uint32_t height,
|
||||
std::uint32_t channels, int quality) {
|
||||
std::uint32_t channels, int quality, Sampling sampling) {
|
||||
if (width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
||||
throw std::invalid_argument("Jpeg::Encode: unsupported image size");
|
||||
}
|
||||
|
|
@ -39,12 +58,18 @@ std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint
|
|||
if (pixels.size() < static_cast<std::size_t>(width) * height * channels) {
|
||||
throw std::invalid_argument("Jpeg::Encode: pixel buffer is too small");
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> output;
|
||||
const int written = stbi_write_jpg_to_func(appendBytes, &output, static_cast<int>(width), static_cast<int>(height),
|
||||
static_cast<int>(channels), pixels.data(), quality);
|
||||
if (written == 0) throw std::runtime_error("Jpeg::Encode: encoding failed");
|
||||
return output;
|
||||
const int subsampling = toSubsampling(sampling);
|
||||
Handle handle;
|
||||
if (tj3Set(handle, TJPARAM_QUALITY, quality) < 0 || tj3Set(handle, TJPARAM_SUBSAMP, channels == 1 ? TJSAMP_GRAY : subsampling) < 0) {
|
||||
throw std::runtime_error("Jpeg::Encode: encoder configuration failed");
|
||||
}
|
||||
Buffer buffer;
|
||||
std::size_t size = 0;
|
||||
const int pixelFormat = channels == 1 ? TJPF_GRAY : TJPF_RGB;
|
||||
if (tj3Compress8(handle, pixels.data(), static_cast<int>(width), 0, static_cast<int>(height), pixelFormat, &buffer.data, &size) < 0) {
|
||||
throw std::runtime_error("Jpeg::Encode: encoding failed");
|
||||
}
|
||||
return {buffer.data, buffer.data + size};
|
||||
}
|
||||
|
||||
std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
static void Require(bool value) { if (!value) std::abort(); }
|
||||
|
|
@ -25,6 +26,25 @@ static int Difference(std::uint8_t left, std::uint8_t right) {
|
|||
return left > right ? left - right : right - left;
|
||||
}
|
||||
|
||||
struct FrameInfo { int components; std::uint8_t sampling; };
|
||||
|
||||
static FrameInfo ReadFrame(const std::vector<std::uint8_t>& jpeg) {
|
||||
std::size_t offset = 2;
|
||||
while (offset + 3 < jpeg.size()) {
|
||||
Require(jpeg[offset++] == 0xFF);
|
||||
while (offset < jpeg.size() && jpeg[offset] == 0xFF) ++offset;
|
||||
Require(offset < jpeg.size());
|
||||
const std::uint8_t marker = jpeg[offset++];
|
||||
if (marker == 0xDA || marker == 0xD9) break;
|
||||
Require(offset + 1 < jpeg.size());
|
||||
const std::size_t length = (static_cast<std::size_t>(jpeg[offset]) << 8) | jpeg[offset + 1];
|
||||
Require(length >= 2 && offset + length <= jpeg.size());
|
||||
if (marker >= 0xC0 && marker <= 0xC3) return {jpeg[offset + 7], jpeg[offset + 9]};
|
||||
offset += length;
|
||||
}
|
||||
std::abort();
|
||||
}
|
||||
|
||||
int main() {
|
||||
constexpr std::uint32_t width = 32;
|
||||
constexpr std::uint32_t height = 24;
|
||||
|
|
@ -41,6 +61,7 @@ int main() {
|
|||
|
||||
const std::vector<std::uint8_t> rgbJpeg = Decoder::Jpeg::Encode(rgb, width, height, 3, 90);
|
||||
Require(IsJpeg(rgbJpeg));
|
||||
Require(ReadFrame(rgbJpeg).components == 3 && ReadFrame(rgbJpeg).sampling == 0x11);
|
||||
const auto rgbImage = Decoder::Jpeg::Decode(rgbJpeg);
|
||||
Require(rgbImage.has_value());
|
||||
Require(rgbImage->width == width && rgbImage->height == height && rgbImage->channels == 3);
|
||||
|
|
@ -55,6 +76,7 @@ int main() {
|
|||
}
|
||||
const std::vector<std::uint8_t> grayJpeg = Decoder::Jpeg::Encode(gray, width, height, 1, 90);
|
||||
Require(IsJpeg(grayJpeg));
|
||||
Require(ReadFrame(grayJpeg).components == 1);
|
||||
const auto grayImage = Decoder::Jpeg::Decode(grayJpeg);
|
||||
Require(grayImage.has_value());
|
||||
Require(grayImage->width == width && grayImage->height == height);
|
||||
|
|
@ -69,6 +91,16 @@ int main() {
|
|||
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, 0x10000, 1, 3, 90); }));
|
||||
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height + 1, 3, 90); }));
|
||||
|
||||
for (const auto [sampling, factor] : {std::pair{Decoder::Jpeg::Sampling::Yuv444, std::uint8_t{0x11}},
|
||||
std::pair{Decoder::Jpeg::Sampling::Yuv422, std::uint8_t{0x21}},
|
||||
std::pair{Decoder::Jpeg::Sampling::Yuv420, std::uint8_t{0x22}}}) {
|
||||
const auto encoded = Decoder::Jpeg::Encode(rgb, 31, 23, 3, 1, sampling);
|
||||
Require(ReadFrame(encoded).components == 3 && ReadFrame(encoded).sampling == factor);
|
||||
const auto highQuality = Decoder::Jpeg::Encode(rgb, 31, 23, 3, 100, sampling);
|
||||
Require(ReadFrame(highQuality).components == 3 && ReadFrame(highQuality).sampling == factor);
|
||||
}
|
||||
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, static_cast<Decoder::Jpeg::Sampling>(3)); }));
|
||||
|
||||
Require(!Decoder::Jpeg::Decode({}).has_value());
|
||||
const std::vector<std::uint8_t> garbage{1, 2, 3, 4, 5, 6, 7, 8};
|
||||
Require(!Decoder::Jpeg::Decode(garbage).has_value());
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ constexpr std::uint8_t SAMPLING_TYPE_420 = 2;
|
|||
constexpr std::uint32_t MAX_IMAGE_DIMENSION = 0xFFFF;
|
||||
constexpr std::uint32_t MAX_IMAGE_PITCH = 0xFFFFFFF;
|
||||
constexpr std::uint64_t MAX_IMAGE_SIZE = 0x7FFFFFFF;
|
||||
constexpr int SUBSAMPLED_MAX_QUALITY = 90;
|
||||
|
||||
struct Encoder {
|
||||
Encoder* self;
|
||||
|
|
@ -148,9 +147,16 @@ std::vector<std::uint8_t> toPackedPixels(const JpegEncEncodeParam& param, std::u
|
|||
}
|
||||
|
||||
int toQuality(const JpegEncEncodeParam& param) {
|
||||
int quality = 100 - param.compression_ratio * 99 / 255;
|
||||
if (param.sampling_type != SAMPLING_TYPE_FULL) quality = std::min(quality, SUBSAMPLED_MAX_QUALITY);
|
||||
return quality;
|
||||
return 100 - param.compression_ratio * 99 / 255;
|
||||
}
|
||||
|
||||
Decoder::Jpeg::Sampling toSampling(const JpegEncEncodeParam& param) {
|
||||
switch (param.sampling_type) {
|
||||
case SAMPLING_TYPE_FULL: return Decoder::Jpeg::Sampling::Yuv444;
|
||||
case SAMPLING_TYPE_422: return Decoder::Jpeg::Sampling::Yuv422;
|
||||
case SAMPLING_TYPE_420: return Decoder::Jpeg::Sampling::Yuv420;
|
||||
}
|
||||
throw std::invalid_argument("sceJpegEncEncode: invalid sampling type");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
@ -187,7 +193,7 @@ int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param
|
|||
|
||||
const std::uint32_t channels = param->pixel_format == PIXEL_FORMAT_Y8 ? 1 : 3;
|
||||
const std::vector<std::uint8_t> pixels = toPackedPixels(*param, channels);
|
||||
const std::vector<std::uint8_t> jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param));
|
||||
const std::vector<std::uint8_t> jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param), toSampling(*param));
|
||||
if (jpeg.size() > param->jpeg_size) return SCE_JPEG_ENC_ERROR_INVALID_SIZE;
|
||||
|
||||
std::memcpy(param->jpeg, jpeg.data(), jpeg.size());
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
|
|
@ -23,17 +24,34 @@ static bool IsJpeg(const unsigned char* data, std::uint32_t size) {
|
|||
return size > 4 && data[0] == 0xFF && data[1] == 0xD8 && data[size - 2] == 0xFF && data[size - 1] == 0xD9;
|
||||
}
|
||||
|
||||
static std::pair<int, std::uint8_t> ReadFrame(const unsigned char* jpeg, std::uint32_t size) {
|
||||
std::size_t offset = 2;
|
||||
while (offset + 3 < size) {
|
||||
Require(jpeg[offset++] == 0xFF);
|
||||
while (offset < size && jpeg[offset] == 0xFF) ++offset;
|
||||
Require(offset < size);
|
||||
const std::uint8_t marker = jpeg[offset++];
|
||||
if (marker == 0xDA || marker == 0xD9) break;
|
||||
Require(offset + 1 < size);
|
||||
const std::size_t length = (static_cast<std::size_t>(jpeg[offset]) << 8) | jpeg[offset + 1];
|
||||
Require(length >= 2 && offset + length <= size);
|
||||
if (marker >= 0xC0 && marker <= 0xC3) return {jpeg[offset + 7], jpeg[offset + 9]};
|
||||
offset += length;
|
||||
}
|
||||
std::abort();
|
||||
}
|
||||
|
||||
static int AverageError(const std::vector<std::uint8_t>& expected, const std::vector<std::uint8_t>& actual) {
|
||||
long total = 0;
|
||||
for (std::size_t i = 0; i < expected.size(); ++i) total += expected[i] > actual[i] ? expected[i] - actual[i] : actual[i] - expected[i];
|
||||
return static_cast<int>(total / static_cast<long>(expected.size()));
|
||||
}
|
||||
|
||||
static std::vector<std::uint8_t> DecodeOutput(const JpegEncOutputInfo& info) {
|
||||
static std::vector<std::uint8_t> DecodeOutput(const JpegEncOutputInfo& info, std::uint32_t channels = 3) {
|
||||
Require(IsJpeg(jpeg, info.size));
|
||||
const auto decoded = Decoder::Jpeg::Decode({jpeg, info.size});
|
||||
Require(decoded.has_value());
|
||||
Require(decoded->width == 16 && decoded->height == 16 && decoded->channels == 3);
|
||||
Require(decoded->width == 16 && decoded->height == 16 && decoded->channels == channels);
|
||||
return decoded->pixels;
|
||||
}
|
||||
|
||||
|
|
@ -169,6 +187,7 @@ int main() {
|
|||
info = {};
|
||||
Require(sceJpegEncEncode(handle, &rgba, &info) == 0);
|
||||
Require(info.height == 16);
|
||||
Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x22}});
|
||||
Require(AverageError(expected, DecodeOutput(info)) < 6);
|
||||
|
||||
for (std::uint32_t y = 0; y < 16; ++y) {
|
||||
|
|
@ -183,6 +202,7 @@ int main() {
|
|||
bgra.pixel_format = 1;
|
||||
info = {};
|
||||
Require(sceJpegEncEncode(handle, &bgra, &info) == 0);
|
||||
Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x22}});
|
||||
Require(AverageError(expected, DecodeOutput(info)) < 6);
|
||||
|
||||
alignas(4) static unsigned char yuyv[16 * 16 * 2];
|
||||
|
|
@ -202,10 +222,8 @@ int main() {
|
|||
Require(AverageError(std::vector<std::uint8_t>(16 * 16 * 3, 100), DecodeOutput(info)) < 3);
|
||||
|
||||
static unsigned char gray[16 * 16];
|
||||
std::vector<std::uint8_t> expectedGray(16 * 16 * 3);
|
||||
for (std::size_t i = 0; i < sizeof(gray); ++i) {
|
||||
gray[i] = static_cast<unsigned char>(((i % 16) + (i / 16)) * 8);
|
||||
for (int c = 0; c < 3; ++c) expectedGray[i * 3 + c] = gray[i];
|
||||
}
|
||||
JpegEncEncodeParam y8 = ValidEncodeParam();
|
||||
y8.image = gray;
|
||||
|
|
@ -216,7 +234,15 @@ int main() {
|
|||
y8.sampling_type = 0;
|
||||
info = {};
|
||||
Require(sceJpegEncEncode(handle, &y8, &info) == 0);
|
||||
Require(AverageError(expectedGray, DecodeOutput(info)) < 6);
|
||||
Require(ReadFrame(jpeg, info.size) == std::pair{1, std::uint8_t{0x11}});
|
||||
const auto decodedGray = DecodeOutput(info, 1);
|
||||
Require(AverageError(std::vector<std::uint8_t>(gray, gray + sizeof(gray)), decodedGray) < 6);
|
||||
|
||||
rgba.sampling_type = 1;
|
||||
rgba.compression_ratio = 0;
|
||||
info = {};
|
||||
Require(sceJpegEncEncode(handle, &rgba, &info) == 0);
|
||||
Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x21}});
|
||||
|
||||
std::memset(jpeg, 0, sizeof(jpeg));
|
||||
Require(sceJpegEncEncode(handle, &rgba, nullptr) == 0);
|
||||
|
|
|
|||
|
|
@ -141,7 +141,6 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [v_mullit_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted, a positive denormal src2 counts as positive): the recompiler does not read MODE, so other IEEE/denormal settings are not modelled.
|
||||
- [v_fma_legacy_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140) is assumed to be fused, which has not been measured on hardware. LLVM decodes this encoding as `v_mad_legacy_f32` for gfx1013, and `v_mac_legacy_f32` (VOP2 0x06) was measured to round its product before the add, so 0x140 may do the same.
|
||||
- The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability.
|
||||
- [libSceJpegEnc](../../core/libs/prx/libSceJpegEnc/Export.cpp) encodes 4:2:2 sampling requests with 4:2:0 chroma subsampling, and grayscale requests as a 3-component JPEG with neutral chroma instead of a single-component one: the [JPEG encoder](../../core/Decoder/Jpeg/src/Jpeg.cpp) (stb) only produces 3-component 4:2:0 and 4:4:4 images.
|
||||
- [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented.
|
||||
- [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. v_sin_f32/v_cos_f32 with inputs beyond ±256 cycles were not compared with the console.
|
||||
- `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. SHA-1 and SHA-256 instructions with a RIP-relative operand fail the relink.
|
||||
|
|
|
|||
Loading…
Reference in New Issue