Resolves TODO(Triang3l) by implementing a mathematical workaround to allow exponent 31 for extended float16 range up to +/-131008.0f as expected by the native Xbox 360 hardware, bypassing standard D3D12 f32tof16 boundaries which incorrectly clamp/inf values.
882 lines
35 KiB
C++
882 lines
35 KiB
C++
/**
|
|
******************************************************************************
|
|
* Xenia : Xbox 360 Emulator Research Project *
|
|
******************************************************************************
|
|
* Copyright 2023 Ben Vanik. All rights reserved. *
|
|
* Released under the BSD license - see LICENSE in the root for more details. *
|
|
******************************************************************************
|
|
*/
|
|
|
|
#include <cstdint>
|
|
|
|
#include "xenia/base/assert.h"
|
|
#include "xenia/base/math.h"
|
|
#include "xenia/gpu/dxbc_shader_translator.h"
|
|
|
|
namespace xe {
|
|
namespace gpu {
|
|
using namespace ucode;
|
|
|
|
void DxbcShaderTranslator::ExportToMemory(uint8_t export_eM) {
|
|
if (!export_eM) {
|
|
return;
|
|
}
|
|
|
|
assert_zero(export_eM & ~current_shader().memexport_eM_written());
|
|
|
|
// Check if memory export is allowed in this invocation.
|
|
a_.OpIf(true, dxbc::Src::R(system_temp_memexport_enabled_and_eM_written_,
|
|
dxbc::Src::kXXXX));
|
|
|
|
// Check if the address with the correct sign and exponent was written, and
|
|
// that the index doesn't overflow the mantissa bits.
|
|
{
|
|
uint32_t address_check_temp = PushSystemTemp();
|
|
a_.OpUShR(dxbc::Dest::R(address_check_temp),
|
|
dxbc::Src::R(system_temp_memexport_address_),
|
|
dxbc::Src::LU(30, 23, 23, 23));
|
|
a_.OpIEq(dxbc::Dest::R(address_check_temp),
|
|
dxbc::Src::R(address_check_temp),
|
|
dxbc::Src::LU(0x1, 0x96, 0x96, 0x96));
|
|
a_.OpAnd(dxbc::Dest::R(address_check_temp, 0b0011),
|
|
dxbc::Src::R(address_check_temp),
|
|
dxbc::Src::R(address_check_temp, 0b1110));
|
|
a_.OpAnd(dxbc::Dest::R(address_check_temp, 0b0001),
|
|
dxbc::Src::R(address_check_temp, dxbc::Src::kXXXX),
|
|
dxbc::Src::R(address_check_temp, dxbc::Src::kYYYY));
|
|
a_.OpIf(true, dxbc::Src::R(address_check_temp, dxbc::Src::kXXXX));
|
|
// Release address_check_temp.
|
|
PopSystemTemp();
|
|
}
|
|
|
|
uint8_t eM_remaining;
|
|
uint32_t eM_index;
|
|
|
|
// Swap red and blue components if needed.
|
|
{
|
|
uint32_t red_blue_swap_temp = PushSystemTemp();
|
|
a_.OpIBFE(dxbc::Dest::R(red_blue_swap_temp, 0b0001), dxbc::Src::LU(1),
|
|
dxbc::Src::LU(19),
|
|
dxbc::Src::R(system_temp_memexport_address_, dxbc::Src::kZZZZ));
|
|
a_.OpIf(true, dxbc::Src::R(red_blue_swap_temp, dxbc::Src::kXXXX));
|
|
// Release red_blue_swap_temp.
|
|
PopSystemTemp();
|
|
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
a_.OpMov(
|
|
dxbc::Dest::R(system_temps_memexport_data_[eM_index], 0b0101),
|
|
dxbc::Src::R(system_temps_memexport_data_[eM_index], 0b11000110));
|
|
}
|
|
|
|
// Close the red/blue swap conditional.
|
|
a_.OpEndIf();
|
|
}
|
|
|
|
uint32_t temp = PushSystemTemp();
|
|
|
|
// Extract the color format and the numeric format.
|
|
// temp.x = color format.
|
|
// temp.y = numeric format is signed.
|
|
// temp.z = numeric format is integer.
|
|
a_.OpUBFE(dxbc::Dest::R(temp, 0b0111), dxbc::Src::LU(6, 1, 1, 0),
|
|
dxbc::Src::LU(8, 16, 17, 0),
|
|
dxbc::Src::R(system_temp_memexport_address_, dxbc::Src::kZZZZ));
|
|
|
|
// Perform format packing.
|
|
// After the switch, temp.x must contain log2 of the number of bytes in an
|
|
// element, of UINT32_MAX if the format is unknown.
|
|
a_.OpSwitch(dxbc::Src::R(temp, dxbc::Src::kXXXX));
|
|
{
|
|
dxbc::Dest element_size_dest(dxbc::Dest::R(temp, 0b0001));
|
|
dxbc::Src num_format_signed(dxbc::Src::R(temp, dxbc::Src::kYYYY));
|
|
dxbc::Src num_format_integer(dxbc::Src::R(temp, dxbc::Src::kZZZZ));
|
|
|
|
auto flush_nan = [this, export_eM](uint32_t components) {
|
|
uint8_t eM_remaining = export_eM;
|
|
uint32_t eM_index;
|
|
uint32_t is_nan_temp = PushSystemTemp();
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpNE(dxbc::Dest::R(is_nan_temp, components), dxbc::Src::R(eM),
|
|
dxbc::Src::R(eM));
|
|
a_.OpMovC(dxbc::Dest::R(eM, components), dxbc::Src::R(is_nan_temp),
|
|
dxbc::Src::LF(0.0f), dxbc::Src::R(eM));
|
|
}
|
|
// Release is_nan_temp.
|
|
PopSystemTemp();
|
|
};
|
|
|
|
// The result will be in eM#.x. The widths must be without holes (R, RG,
|
|
// RGB, RGBA), and expecting the widths to add up to the size of the stored
|
|
// texel (8, 16 or 32 bits), as the unused upper bits will contain junk from
|
|
// the sign extension of X if the number is signed.
|
|
auto pack_8_16_32 = [&](std::array<uint32_t, 4> widths) {
|
|
uint8_t eM_remaining;
|
|
uint32_t eM_index;
|
|
|
|
uint32_t components = 0;
|
|
std::array<uint32_t, 4> offsets = {};
|
|
for (uint32_t i = 0; i < 4; ++i) {
|
|
if (widths[i]) {
|
|
// Only formats for which max + 0.5 can be represented exactly.
|
|
assert(widths[i] <= 23);
|
|
components |= uint32_t(1) << i;
|
|
}
|
|
if (i) {
|
|
offsets[i] = offsets[i - 1] + widths[i - 1];
|
|
}
|
|
}
|
|
// Will be packing components into eM#.x starting from green, assume red
|
|
// will already be there after the conversion.
|
|
assert_not_zero(components & 0b1);
|
|
|
|
flush_nan(components);
|
|
|
|
a_.OpIf(true, num_format_signed);
|
|
{
|
|
// Signed.
|
|
a_.OpIf(true, num_format_integer);
|
|
{
|
|
// Signed integer.
|
|
float min_value[4] = {}, max_value[4] = {};
|
|
for (uint32_t i = 0; i < 4; ++i) {
|
|
if (widths[i]) {
|
|
max_value[i] = float((uint32_t(1) << (widths[i] - 1)) - 1);
|
|
min_value[i] = -1.0f - max_value[i];
|
|
}
|
|
}
|
|
dxbc::Src min_value_src(dxbc::Src::LP(min_value));
|
|
dxbc::Src max_value_src(dxbc::Src::LP(max_value));
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpMax(dxbc::Dest::R(eM, components), min_value_src,
|
|
dxbc::Src::R(eM));
|
|
a_.OpMin(dxbc::Dest::R(eM, components), max_value_src,
|
|
dxbc::Src::R(eM));
|
|
}
|
|
}
|
|
a_.OpElse();
|
|
{
|
|
// Signed normalized.
|
|
uint32_t scale_components = 0;
|
|
float scale[4] = {};
|
|
for (uint32_t i = 0; i < 4; ++i) {
|
|
if (widths[i] > 2) {
|
|
scale_components |= uint32_t(1) << i;
|
|
scale[i] = float((uint32_t(1) << (widths[i] - 1)) - 1);
|
|
}
|
|
}
|
|
dxbc::Src scale_src(dxbc::Src::LP(scale));
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpMax(dxbc::Dest::R(eM, components), dxbc::Src::LF(-1.0f),
|
|
dxbc::Src::R(eM));
|
|
a_.OpMin(dxbc::Dest::R(eM, components), dxbc::Src::LF(1.0f),
|
|
dxbc::Src::R(eM));
|
|
if (scale_components) {
|
|
a_.OpMul(dxbc::Dest::R(eM, scale_components), dxbc::Src::R(eM),
|
|
scale_src);
|
|
}
|
|
}
|
|
}
|
|
a_.OpEndIf();
|
|
|
|
// Add plus/minus 0.5 before truncating according to the Direct3D format
|
|
// conversion rules, and convert to signed integers.
|
|
uint32_t round_bias_temp = PushSystemTemp();
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpBFI(dxbc::Dest::R(eM, components), dxbc::Src::LU(31),
|
|
dxbc::Src::LU(0), dxbc::Src::LF(0.5f), dxbc::Src::R(eM));
|
|
a_.OpAdd(dxbc::Dest::R(eM, components), dxbc::Src::R(eM),
|
|
dxbc::Src::R(round_bias_temp));
|
|
a_.OpFToI(dxbc::Dest::R(eM, components), dxbc::Src::R(eM));
|
|
}
|
|
// Release round_bias_temp.
|
|
PopSystemTemp();
|
|
}
|
|
a_.OpElse();
|
|
{
|
|
// Unsigned.
|
|
a_.OpIf(true, num_format_integer);
|
|
{
|
|
// Unsigned integer.
|
|
float max_value[4];
|
|
for (uint32_t i = 0; i < 4; ++i) {
|
|
max_value[i] = float((uint32_t(1) << widths[i]) - 1);
|
|
}
|
|
dxbc::Src max_value_src(dxbc::Src::LP(max_value));
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpMax(dxbc::Dest::R(eM, components), dxbc::Src::LF(0.0f),
|
|
dxbc::Src::R(eM));
|
|
a_.OpMin(dxbc::Dest::R(eM, components), max_value_src,
|
|
dxbc::Src::R(eM));
|
|
}
|
|
}
|
|
a_.OpElse();
|
|
{
|
|
// Unsigned normalized.
|
|
uint32_t scale_components = 0;
|
|
float scale[4] = {};
|
|
for (uint32_t i = 0; i < 4; ++i) {
|
|
if (widths[i] > 1) {
|
|
scale_components |= uint32_t(1) << i;
|
|
scale[i] = float((uint32_t(1) << widths[i]) - 1);
|
|
}
|
|
}
|
|
dxbc::Src scale_src(dxbc::Src::LP(scale));
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
// Saturate.
|
|
a_.OpMov(dxbc::Dest::R(eM, components), dxbc::Src::R(eM), true);
|
|
if (scale_components) {
|
|
a_.OpMul(dxbc::Dest::R(eM, scale_components), dxbc::Src::R(eM),
|
|
scale_src);
|
|
}
|
|
}
|
|
}
|
|
a_.OpEndIf();
|
|
|
|
// Add 0.5 before truncating according to the Direct3D format conversion
|
|
// rules, and convert to unsigned integers.
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpAdd(dxbc::Dest::R(eM, components), dxbc::Src::R(eM),
|
|
dxbc::Src::LF(0.5f));
|
|
a_.OpFToU(dxbc::Dest::R(eM, components), dxbc::Src::R(eM));
|
|
}
|
|
}
|
|
a_.OpEndIf();
|
|
|
|
// Pack into 32 bits.
|
|
for (uint32_t i = 0; i < 4; ++i) {
|
|
if (!widths[i]) {
|
|
continue;
|
|
}
|
|
dxbc::Src width_src(dxbc::Src::LU(widths[i]));
|
|
dxbc::Src offset_src(dxbc::Src::LU(offsets[i]));
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpBFI(dxbc::Dest::R(eM, 0b0001), width_src, offset_src,
|
|
dxbc::Src::R(eM).Select(i),
|
|
dxbc::Src::R(eM, dxbc::Src::kXXXX));
|
|
}
|
|
}
|
|
};
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_8)));
|
|
// TODO(Triang3l): Investigate how input should be treated for k_8_A, k_8_B,
|
|
// k_8_8_8_8_A.
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_8_A)));
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_8_B)));
|
|
{
|
|
pack_8_16_32({8});
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(0));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_1_5_5_5)));
|
|
{
|
|
pack_8_16_32({5, 5, 5, 1});
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(1));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_5_6_5)));
|
|
{
|
|
pack_8_16_32({5, 6, 5});
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(1));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_6_5_5)));
|
|
{
|
|
pack_8_16_32({5, 5, 6});
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(1));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_8_8_8_8)));
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_8_8_8_8_A)));
|
|
a_.OpCase(
|
|
dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_8_8_8_8_AS_16_16_16_16)));
|
|
{
|
|
pack_8_16_32({8, 8, 8, 8});
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(2));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_2_10_10_10)));
|
|
a_.OpCase(dxbc::Src::LU(
|
|
uint32_t(xenos::ColorFormat::k_2_10_10_10_AS_16_16_16_16)));
|
|
{
|
|
pack_8_16_32({10, 10, 10, 2});
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(2));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_8_8)));
|
|
{
|
|
pack_8_16_32({8, 8});
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(1));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_4_4_4_4)));
|
|
{
|
|
pack_8_16_32({4, 4, 4, 4});
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(1));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_10_11_11)));
|
|
a_.OpCase(
|
|
dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_10_11_11_AS_16_16_16_16)));
|
|
{
|
|
pack_8_16_32({11, 11, 10});
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(2));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_11_11_10)));
|
|
a_.OpCase(
|
|
dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_11_11_10_AS_16_16_16_16)));
|
|
{
|
|
pack_8_16_32({10, 11, 11});
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(2));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_16)));
|
|
{
|
|
pack_8_16_32({16});
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(1));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_16_16)));
|
|
{
|
|
pack_8_16_32({16, 16});
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(2));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_16_16_16_16)));
|
|
{
|
|
flush_nan(0b1111);
|
|
|
|
a_.OpIf(true, num_format_signed);
|
|
{
|
|
// Signed.
|
|
a_.OpIf(true, num_format_integer);
|
|
{
|
|
// Signed integer.
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpMax(dxbc::Dest::R(eM), dxbc::Src::LF(float(INT16_MIN)),
|
|
dxbc::Src::R(eM));
|
|
a_.OpMin(dxbc::Dest::R(eM), dxbc::Src::LF(float(INT16_MAX)),
|
|
dxbc::Src::R(eM));
|
|
}
|
|
}
|
|
a_.OpElse();
|
|
{
|
|
// Signed normalized.
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpMax(dxbc::Dest::R(eM), dxbc::Src::LF(-1.0f), dxbc::Src::R(eM));
|
|
a_.OpMin(dxbc::Dest::R(eM), dxbc::Src::LF(1.0f), dxbc::Src::R(eM));
|
|
a_.OpMul(dxbc::Dest::R(eM), dxbc::Src::R(eM),
|
|
dxbc::Src::LF(float(INT16_MAX)));
|
|
}
|
|
}
|
|
a_.OpEndIf();
|
|
|
|
// Add plus/minus 0.5 before truncating according to the Direct3D format
|
|
// conversion rules, and convert to signed integers.
|
|
uint32_t round_bias_temp = PushSystemTemp();
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpBFI(dxbc::Dest::R(eM), dxbc::Src::LU(31), dxbc::Src::LU(0),
|
|
dxbc::Src::LF(0.5f), dxbc::Src::R(eM));
|
|
a_.OpAdd(dxbc::Dest::R(eM), dxbc::Src::R(eM),
|
|
dxbc::Src::R(round_bias_temp));
|
|
a_.OpFToI(dxbc::Dest::R(eM), dxbc::Src::R(eM));
|
|
}
|
|
// Release round_bias_temp.
|
|
PopSystemTemp();
|
|
}
|
|
a_.OpElse();
|
|
{
|
|
// Unsigned.
|
|
a_.OpIf(true, num_format_integer);
|
|
{
|
|
// Unsigned integer.
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpMax(dxbc::Dest::R(eM), dxbc::Src::LF(0.0f), dxbc::Src::R(eM));
|
|
a_.OpMin(dxbc::Dest::R(eM), dxbc::Src::LF(float(UINT16_MAX)),
|
|
dxbc::Src::R(eM));
|
|
}
|
|
}
|
|
a_.OpElse();
|
|
{
|
|
// Unsigned normalized.
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
// Saturate.
|
|
a_.OpMov(dxbc::Dest::R(eM), dxbc::Src::R(eM), true);
|
|
a_.OpMul(dxbc::Dest::R(eM), dxbc::Src::R(eM),
|
|
dxbc::Src::LF(float(UINT16_MAX)));
|
|
}
|
|
}
|
|
a_.OpEndIf();
|
|
|
|
// Add 0.5 before truncating according to the Direct3D format conversion
|
|
// rules, and convert to unsigned integers.
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpAdd(dxbc::Dest::R(eM), dxbc::Src::R(eM), dxbc::Src::LF(0.5f));
|
|
a_.OpFToU(dxbc::Dest::R(eM), dxbc::Src::R(eM));
|
|
}
|
|
}
|
|
a_.OpEndIf();
|
|
|
|
// Pack.
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpBFI(dxbc::Dest::R(eM, 0b0011), dxbc::Src::LU(16),
|
|
dxbc::Src::LU(16), dxbc::Src::R(eM, 0b1101),
|
|
dxbc::Src::R(eM, 0b1000));
|
|
}
|
|
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(3));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
// Xbox 360 float16 uses extended range: exponent 31 is NOT Inf/NaN
|
|
// but a valid large value (up to ±131008). Standard DXBC f32tof16
|
|
// clamps to ±65504 and produces Inf for larger values. This helper
|
|
// detects where standard conversion overflowed to Inf/NaN and
|
|
// re-encodes those values using the extended range representation:
|
|
// halve the value, convert to standard f16 (giving exponent <= 30),
|
|
// then increment the exponent by 1 (placing it in the exponent 31
|
|
// slot that Xbox 360 treats as a normal value, not Inf/NaN).
|
|
auto f32_to_f16_extended_range = [&](uint32_t components) {
|
|
uint32_t ext_original_temp = PushSystemTemp();
|
|
uint32_t ext_mask_temp = PushSystemTemp();
|
|
|
|
uint8_t eM_remaining_ext = export_eM;
|
|
uint32_t eM_index_ext;
|
|
while (xe::bit_scan_forward(eM_remaining_ext, &eM_index_ext)) {
|
|
eM_remaining_ext &= ~(uint8_t(1) << eM_index_ext);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index_ext];
|
|
|
|
// Save original float32 values before conversion.
|
|
a_.OpMov(dxbc::Dest::R(ext_original_temp, components),
|
|
dxbc::Src::R(eM));
|
|
|
|
// Standard f32 to f16 conversion (handles ±0..65504 correctly).
|
|
a_.OpF32ToF16(dxbc::Dest::R(eM, components), dxbc::Src::R(eM));
|
|
|
|
// Detect where standard conversion produced Inf or NaN
|
|
// (exponent field = 31, i.e. bits [14:10] all set = 0x7C00).
|
|
a_.OpAnd(dxbc::Dest::R(ext_mask_temp, components), dxbc::Src::R(eM),
|
|
dxbc::Src::LU(0x7C00));
|
|
a_.OpIEq(dxbc::Dest::R(ext_mask_temp, components),
|
|
dxbc::Src::R(ext_mask_temp), dxbc::Src::LU(0x7C00));
|
|
|
|
// For values that overflowed, compute extended range encoding
|
|
// from the saved original float32 values:
|
|
// 1. Clamp to ±131008.0 (max extended float16 can represent).
|
|
a_.OpMin(dxbc::Dest::R(ext_original_temp, components),
|
|
dxbc::Src::R(ext_original_temp), dxbc::Src::LF(131008.0f));
|
|
a_.OpMax(dxbc::Dest::R(ext_original_temp, components),
|
|
dxbc::Src::R(ext_original_temp), dxbc::Src::LF(-131008.0f));
|
|
// 2. Halve to bring into standard float16 range (max 65504).
|
|
a_.OpMul(dxbc::Dest::R(ext_original_temp, components),
|
|
dxbc::Src::R(ext_original_temp), dxbc::Src::LF(0.5f));
|
|
// 3. Convert the halved value (will have exponent <= 30).
|
|
a_.OpF32ToF16(dxbc::Dest::R(ext_original_temp, components),
|
|
dxbc::Src::R(ext_original_temp));
|
|
// 4. Increment exponent by 1 (add 1 << 10 = 0x0400) to
|
|
// compensate for the halving → places value at exponent 31.
|
|
a_.OpIAdd(dxbc::Dest::R(ext_original_temp, components),
|
|
dxbc::Src::R(ext_original_temp), dxbc::Src::LU(0x0400));
|
|
|
|
// Select: use extended result where standard gave Inf/NaN,
|
|
// keep standard result otherwise.
|
|
a_.OpMovC(dxbc::Dest::R(eM, components), dxbc::Src::R(ext_mask_temp),
|
|
dxbc::Src::R(ext_original_temp), dxbc::Src::R(eM));
|
|
}
|
|
|
|
// Release ext_mask_temp and ext_original_temp.
|
|
PopSystemTemp(2);
|
|
};
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_16_FLOAT)));
|
|
{
|
|
f32_to_f16_extended_range(0b0001);
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(1));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_16_16_FLOAT)));
|
|
{
|
|
f32_to_f16_extended_range(0b0011);
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpBFI(dxbc::Dest::R(eM, 0b0001), dxbc::Src::LU(16),
|
|
dxbc::Src::LU(16), dxbc::Src::R(eM, dxbc::Src::kYYYY),
|
|
dxbc::Src::R(eM, dxbc::Src::kXXXX));
|
|
}
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(2));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_16_16_16_16_FLOAT)));
|
|
{
|
|
f32_to_f16_extended_range(0b1111);
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpBFI(dxbc::Dest::R(eM, 0b0011), dxbc::Src::LU(16),
|
|
dxbc::Src::LU(16), dxbc::Src::R(eM, 0b1101),
|
|
dxbc::Src::R(eM, 0b1000));
|
|
}
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(3));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_32_FLOAT)));
|
|
{
|
|
// Already in eM#.
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(2));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_32_32_FLOAT)));
|
|
{
|
|
// Already in eM#.
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(3));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::ColorFormat::k_32_32_32_32_FLOAT)));
|
|
{
|
|
// Already in eM#.
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(4));
|
|
}
|
|
a_.OpBreak();
|
|
|
|
a_.OpDefault();
|
|
a_.OpMov(element_size_dest, dxbc::Src::LU(UINT32_MAX));
|
|
a_.OpBreak();
|
|
}
|
|
// Close the color format switch.
|
|
a_.OpEndSwitch();
|
|
|
|
dxbc::Src element_size_src(dxbc::Src::R(temp, dxbc::Src::kXXXX));
|
|
|
|
// Only temp.x is used currently (for the element size log2).
|
|
|
|
// Do endian swap, using temp.y for the endianness value, and temp.z as a
|
|
// temporary value.
|
|
{
|
|
dxbc::Dest endian_dest(dxbc::Dest::R(temp, 0b0010));
|
|
dxbc::Src endian_src(dxbc::Src::R(temp, dxbc::Src::kYYYY));
|
|
// Extract endianness into temp.y.
|
|
a_.OpUBFE(endian_dest, dxbc::Src::LU(3), dxbc::Src::LU(0),
|
|
dxbc::Src::R(system_temp_memexport_address_, dxbc::Src::kZZZZ));
|
|
|
|
// Change 8-in-64 and 8-in-128 to 8-in-32.
|
|
for (uint32_t i = 0; i < 2; ++i) {
|
|
a_.OpIEq(dxbc::Dest::R(temp, 0b0100), endian_src,
|
|
dxbc::Src::LU(uint32_t(i ? xenos::Endian128::k8in128
|
|
: xenos::Endian128::k8in64)));
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
a_.OpMovC(dxbc::Dest::R(eM), dxbc::Src::R(temp, dxbc::Src::kZZZZ),
|
|
dxbc::Src::R(eM, i ? 0b00011011 : 0b10110001),
|
|
dxbc::Src::R(eM));
|
|
}
|
|
a_.OpMovC(endian_dest, dxbc::Src::R(temp, dxbc::Src::kZZZZ),
|
|
dxbc::Src::LU(uint32_t(xenos::Endian128::k8in32)), endian_src);
|
|
}
|
|
|
|
uint32_t swap_temp = PushSystemTemp();
|
|
dxbc::Dest swap_temp_dest(dxbc::Dest::R(swap_temp));
|
|
dxbc::Src swap_temp_src(dxbc::Src::R(swap_temp));
|
|
|
|
// 8-in-16 or one half of 8-in-32.
|
|
a_.OpSwitch(endian_src);
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::Endian128::k8in16)));
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::Endian128::k8in32)));
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
dxbc::Dest eM_dest(dxbc::Dest::R(eM));
|
|
dxbc::Src eM_src(dxbc::Src::R(eM));
|
|
// Temp = X0Z0.
|
|
a_.OpAnd(swap_temp_dest, eM_src, dxbc::Src::LU(0x00FF00FF));
|
|
// eM = YZW0.
|
|
a_.OpUShR(eM_dest, eM_src, dxbc::Src::LU(8));
|
|
// eM = Y0W0.
|
|
a_.OpAnd(eM_dest, eM_src, dxbc::Src::LU(0x00FF00FF));
|
|
// eM = YXWZ.
|
|
a_.OpUMAd(eM_dest, swap_temp_src, dxbc::Src::LU(256), eM_src);
|
|
}
|
|
a_.OpBreak();
|
|
a_.OpEndSwitch();
|
|
|
|
// 16-in-32 or another half of 8-in-32.
|
|
a_.OpSwitch(endian_src);
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::Endian128::k8in32)));
|
|
a_.OpCase(dxbc::Src::LU(uint32_t(xenos::Endian128::k16in32)));
|
|
eM_remaining = export_eM;
|
|
while (xe::bit_scan_forward(eM_remaining, &eM_index)) {
|
|
eM_remaining &= ~(uint8_t(1) << eM_index);
|
|
uint32_t eM = system_temps_memexport_data_[eM_index];
|
|
dxbc::Dest eM_dest(dxbc::Dest::R(eM));
|
|
dxbc::Src eM_src(dxbc::Src::R(eM));
|
|
// Temp = ZW00.
|
|
a_.OpUShR(swap_temp_dest, eM_src, dxbc::Src::LU(16));
|
|
// eM = ZWXY.
|
|
a_.OpBFI(eM_dest, dxbc::Src::LU(16), dxbc::Src::LU(16), eM_src,
|
|
swap_temp_src);
|
|
}
|
|
a_.OpBreak();
|
|
a_.OpEndSwitch();
|
|
|
|
// Release swap_temp.
|
|
PopSystemTemp();
|
|
}
|
|
|
|
// Extract the base index to temp.y and the index upper bound to temp.z.
|
|
a_.OpUBFE(dxbc::Dest::R(temp, 0b0110), dxbc::Src::LU(23), dxbc::Src::LU(0),
|
|
dxbc::Src::R(system_temp_memexport_address_, 0b1101 << 2));
|
|
dxbc::Dest eM0_address_dest(dxbc::Dest::R(temp, 0b0010));
|
|
dxbc::Src eM0_address_src(dxbc::Src::R(temp, dxbc::Src::kYYYY));
|
|
dxbc::Src index_count_src(dxbc::Src::R(temp, dxbc::Src::kZZZZ));
|
|
|
|
// Check if eM0 isn't out of bounds via temp.w - if it is, eM1...4 also are
|
|
// (the base index can't be negative).
|
|
a_.OpILT(dxbc::Dest::R(temp, 0b1000), eM0_address_src, index_count_src);
|
|
a_.OpIf(true, dxbc::Src::R(temp, dxbc::Src::kWWWW));
|
|
|
|
// Extract the base address to temp.w as bytes (30 lower bits to 30 upper bits
|
|
// with 0 below).
|
|
a_.OpIShL(dxbc::Dest::R(temp, 0b1000),
|
|
dxbc::Src::R(system_temp_memexport_address_, dxbc::Src::kXXXX),
|
|
dxbc::Src::LU(2));
|
|
dxbc::Src base_address_src(dxbc::Src::R(temp, dxbc::Src::kWWWW));
|
|
|
|
uint8_t export_eM14 = export_eM >> 1;
|
|
assert_zero(export_eM14 >> 4);
|
|
uint32_t eM14_address_temp = UINT32_MAX, store_eM14_temp = UINT32_MAX;
|
|
if (export_eM14) {
|
|
// Get eM1...4 indices and check if they're in bounds.
|
|
eM14_address_temp = PushSystemTemp();
|
|
dxbc::Dest eM14_address_dest(dxbc::Dest::R(eM14_address_temp, export_eM14));
|
|
dxbc::Src eM14_address_src(dxbc::Src::R(eM14_address_temp));
|
|
store_eM14_temp = PushSystemTemp();
|
|
dxbc::Dest store_eM14_dest(dxbc::Dest::R(store_eM14_temp, export_eM14));
|
|
dxbc::Src store_eM14_src(dxbc::Src::R(store_eM14_temp));
|
|
a_.OpIAdd(eM14_address_dest, eM0_address_src, dxbc::Src::LU(1, 2, 3, 4));
|
|
a_.OpILT(store_eM14_dest, eM14_address_src, index_count_src);
|
|
// Check if eM1...4 were actually written by the invocation and merge the
|
|
// result with store_eM14_temp.
|
|
uint32_t eM14_written_temp = PushSystemTemp();
|
|
a_.OpIBFE(dxbc::Dest::R(eM14_written_temp, export_eM14), dxbc::Src::LU(1),
|
|
dxbc::Src::LU(1, 2, 3, 4),
|
|
dxbc::Src::R(system_temp_memexport_enabled_and_eM_written_,
|
|
dxbc::Src::kYYYY));
|
|
a_.OpAnd(store_eM14_dest, store_eM14_src, dxbc::Src::R(eM14_written_temp));
|
|
// Release eM14_written_temp.
|
|
PopSystemTemp();
|
|
// Convert eM1...4 indices to global byte addresses.
|
|
a_.OpIShL(eM14_address_dest, eM14_address_src, element_size_src);
|
|
a_.OpIAdd(eM14_address_dest, base_address_src, eM14_address_src);
|
|
}
|
|
if (export_eM & 0b1) {
|
|
// Convert eM0 index to a global byte address if it's needed.
|
|
a_.OpIShL(eM0_address_dest, eM0_address_src, element_size_src);
|
|
a_.OpIAdd(eM0_address_dest, base_address_src, eM0_address_src);
|
|
// base_address_src and index_count_src are deallocated at this point (even
|
|
// if eM0 isn't potentially written), temp.zw are now free.
|
|
// Extract if eM0 was actually written by the invocation to temp.z.
|
|
a_.OpIBFE(dxbc::Dest::R(temp, 0b0100), dxbc::Src::LU(1), dxbc::Src::LU(0),
|
|
dxbc::Src::R(system_temp_memexport_enabled_and_eM_written_,
|
|
dxbc::Src::kYYYY));
|
|
}
|
|
dxbc::Src eM0_written_src(dxbc::Src::R(temp, dxbc::Src::kZZZZ));
|
|
|
|
// Write depending on the element size.
|
|
// No switch case will be entered for an unknown format (UINT32_MAX size
|
|
// written), so writing won't be attempted for it.
|
|
if (uav_index_shared_memory_ == kBindingIndexUnallocated) {
|
|
uav_index_shared_memory_ = uav_count_++;
|
|
}
|
|
uint8_t eM14_remaining;
|
|
uint32_t eM14_index;
|
|
a_.OpSwitch(element_size_src);
|
|
|
|
// 8bpp, 16bpp.
|
|
dxbc::Dest atomic_dest(dxbc::Dest::U(
|
|
uav_index_shared_memory_, uint32_t(UAVRegister::kSharedMemory), 0));
|
|
for (uint32_t i = 0; i <= 1; ++i) {
|
|
a_.OpCase(dxbc::Src::LU(i));
|
|
dxbc::Src width_src(dxbc::Src::LU(8 << i));
|
|
uint32_t sub_dword_temp = PushSystemTemp();
|
|
if (export_eM & 0b1) {
|
|
a_.OpIf(true, eM0_written_src);
|
|
// sub_dword_temp.x = eM0 offset in the dword (8 << (byte_address & 3))
|
|
// (assuming a little-endian host).
|
|
a_.OpBFI(dxbc::Dest::R(sub_dword_temp, 0b0001), dxbc::Src::LU(2),
|
|
dxbc::Src::LU(3), eM0_address_src, dxbc::Src::LU(0));
|
|
// Keep only the dword part of the address.
|
|
a_.OpAnd(eM0_address_dest, eM0_address_src, dxbc::Src::LU(~uint32_t(3)));
|
|
// Erase the bits that will be replaced with eM0 via sub_dword_temp.y.
|
|
a_.OpBFI(dxbc::Dest::R(sub_dword_temp, 0b0010), width_src,
|
|
dxbc::Src::R(sub_dword_temp, dxbc::Src::kXXXX), dxbc::Src::LU(0),
|
|
dxbc::Src::LU(UINT32_MAX));
|
|
a_.OpAtomicAnd(atomic_dest, eM0_address_src, 0b0001,
|
|
dxbc::Src::R(sub_dword_temp, dxbc::Src::kYYYY));
|
|
// Add the eM0 bits via sub_dword_temp.y.
|
|
a_.OpBFI(dxbc::Dest::R(sub_dword_temp, 0b0010), width_src,
|
|
dxbc::Src::R(sub_dword_temp, dxbc::Src::kXXXX),
|
|
dxbc::Src::R(system_temps_memexport_data_[0], dxbc::Src::kXXXX),
|
|
dxbc::Src::LU(0));
|
|
a_.OpAtomicOr(atomic_dest, eM0_address_src, 0b0001,
|
|
dxbc::Src::R(sub_dword_temp, dxbc::Src::kYYYY));
|
|
a_.OpEndIf();
|
|
}
|
|
if (export_eM14) {
|
|
// sub_dword_temp = eM# offset in the dword (8 << (byte_address & 3))
|
|
// (assuming a little-endian host).
|
|
a_.OpBFI(dxbc::Dest::R(sub_dword_temp, export_eM14), dxbc::Src::LU(2),
|
|
dxbc::Src::LU(3), dxbc::Src::R(eM14_address_temp),
|
|
dxbc::Src::LU(0));
|
|
// Keep only the dword part of the address.
|
|
a_.OpAnd(dxbc::Dest::R(eM14_address_temp, export_eM14),
|
|
dxbc::Src::R(eM14_address_temp), dxbc::Src::LU(~uint32_t(3)));
|
|
uint32_t sub_dword_data_temp = PushSystemTemp();
|
|
eM14_remaining = export_eM14;
|
|
while (xe::bit_scan_forward(eM14_remaining, &eM14_index)) {
|
|
eM14_remaining &= ~(uint8_t(1) << eM14_index);
|
|
a_.OpIf(true, dxbc::Src::R(store_eM14_temp).Select(eM14_index));
|
|
// Erase the bits that will be replaced with eM# via
|
|
// sub_dword_data_temp.x.
|
|
a_.OpBFI(dxbc::Dest::R(sub_dword_data_temp, 0b0001), width_src,
|
|
dxbc::Src::R(sub_dword_temp).Select(eM14_index),
|
|
dxbc::Src::LU(0), dxbc::Src::LU(UINT32_MAX));
|
|
a_.OpAtomicAnd(
|
|
atomic_dest, dxbc::Src::R(eM14_address_temp).Select(eM14_index),
|
|
0b0001, dxbc::Src::R(sub_dword_data_temp, dxbc::Src::kXXXX));
|
|
// Add the eM# bits via sub_dword_temp.y.
|
|
a_.OpBFI(dxbc::Dest::R(sub_dword_data_temp, 0b0001), width_src,
|
|
dxbc::Src::R(sub_dword_temp).Select(eM14_index),
|
|
dxbc::Src::R(system_temps_memexport_data_[1 + eM14_index],
|
|
dxbc::Src::kXXXX),
|
|
dxbc::Src::LU(0));
|
|
a_.OpAtomicOr(
|
|
atomic_dest, dxbc::Src::R(eM14_address_temp).Select(eM14_index),
|
|
0b0001, dxbc::Src::R(sub_dword_data_temp, dxbc::Src::kXXXX));
|
|
a_.OpEndIf();
|
|
}
|
|
// Release sub_dword_data_temp.
|
|
PopSystemTemp();
|
|
}
|
|
// Release sub_dword_temp.
|
|
PopSystemTemp();
|
|
a_.OpBreak();
|
|
}
|
|
|
|
// 32bpp, 64bpp, 128bpp.
|
|
for (uint32_t i = 2; i <= 4; ++i) {
|
|
a_.OpCase(dxbc::Src::LU(i));
|
|
// Store (0b0001), Store2 (0b0011), Store4 (0b1111).
|
|
uint32_t store_mask = (uint32_t(1) << (uint32_t(1) << (i - 2))) - 1;
|
|
dxbc::Dest store_dest(dxbc::Dest::U(uav_index_shared_memory_,
|
|
uint32_t(UAVRegister::kSharedMemory),
|
|
store_mask));
|
|
if (export_eM & 0b1) {
|
|
a_.OpIf(true, eM0_written_src);
|
|
a_.OpStoreRaw(store_dest, eM0_address_src,
|
|
dxbc::Src::R(system_temps_memexport_data_[0]));
|
|
a_.OpEndIf();
|
|
}
|
|
eM14_remaining = export_eM14;
|
|
while (xe::bit_scan_forward(eM14_remaining, &eM14_index)) {
|
|
eM14_remaining &= ~(uint8_t(1) << eM14_index);
|
|
a_.OpIf(true, dxbc::Src::R(store_eM14_temp).Select(eM14_index));
|
|
a_.OpStoreRaw(store_dest,
|
|
dxbc::Src::R(eM14_address_temp).Select(eM14_index),
|
|
dxbc::Src::R(system_temps_memexport_data_[1 + eM14_index]));
|
|
a_.OpEndIf();
|
|
}
|
|
a_.OpBreak();
|
|
}
|
|
|
|
// Close the element size switch.
|
|
a_.OpEndSwitch();
|
|
|
|
if (export_eM14) {
|
|
// Release eM14_address_temp and store_eM14_temp.
|
|
PopSystemTemp(2);
|
|
}
|
|
|
|
// Close the eM0 bounds check.
|
|
a_.OpEndIf();
|
|
|
|
// Release temp.
|
|
PopSystemTemp();
|
|
|
|
// Close the address correctness conditional.
|
|
a_.OpEndIf();
|
|
|
|
// Close the memory export allowed conditional.
|
|
a_.OpEndIf();
|
|
}
|
|
|
|
} // namespace gpu
|
|
} // namespace xe
|