/**
 ******************************************************************************
 * Xenia : Xbox 360 Emulator Research Project                                 *
 ******************************************************************************
 * Copyright 2022 Ben Vanik. All rights reserved.                             *
 * Released under the BSD license - see LICENSE in the root for more details. *
 ******************************************************************************
 */

#ifndef XENIA_GPU_SHADERS_TEXTURE_ADDRESS_XESLI_
#define XENIA_GPU_SHADERS_TEXTURE_ADDRESS_XESLI_

#include "../../ui/shaders/xesl.xesli"

#define XENOS_TEXTURE_MACRO_TILE_WIDTH_LOG2 5
#define XENOS_TEXTURE_MACRO_TILE_HEIGHT_2D_LOG2 5
#define XENOS_TEXTURE_MACRO_TILE_HEIGHT_3D_LOG2 4
#define XENOS_TEXTURE_MACRO_TILE_DEPTH_LOG2 2

int XenosTextureTiledAddressCombine(const int outer_inner_bytes, const int bank,
                                    const int pipe, const int y_lsb) {
  return (y_lsb << 4) | (pipe << 6) | (bank << 11) | (outer_inner_bytes & 0xF) |
         (((outer_inner_bytes >> 4) & 0x1) << 5) |
         (((outer_inner_bytes >> 5) & 0x7) << 8) |
         (outer_inner_bytes >> 8 << 12);
}

int XenosTextureTiledAddress2D(const int2_xe p, const uint pitch_macro_tiles,
                               const uint bytes_per_block_log2) {
  const int outer_blocks =
      ((p.y >> XENOS_TEXTURE_MACRO_TILE_HEIGHT_2D_LOG2) *
           int(pitch_macro_tiles) +
       (p.x >> XENOS_TEXTURE_MACRO_TILE_WIDTH_LOG2))
      << 6;
  const int inner_blocks = (((p.y >> 1) & 0x7) << 3) | (p.x & 0x7);
  const int outer_inner_bytes = (outer_blocks | inner_blocks)
                                << bytes_per_block_log2;
  const int bank = (p.y >> 4) & 0x1;
  const int pipe = ((p.x >> 3) & 0x3) ^ (((p.y >> 3) & 0x1) << 1);
  return XenosTextureTiledAddressCombine(outer_inner_bytes, bank, pipe,
                                         p.y & 1);
}

int XenosTextureTiledAddress3D(const int3_xe p, const uint pitch_macro_tiles,
                               const uint height_macro_tiles,
                               const uint bytes_per_block_log2) {
  const int outer_blocks =
      ((((p.z >> XENOS_TEXTURE_MACRO_TILE_DEPTH_LOG2) *
             int(height_macro_tiles) +
         (p.y >> XENOS_TEXTURE_MACRO_TILE_HEIGHT_3D_LOG2)) *
        int(pitch_macro_tiles)) +
       (p.x >> XENOS_TEXTURE_MACRO_TILE_WIDTH_LOG2))
      << 7;
  const int inner_blocks =
      ((p.z & 0x3) << 5) | (((p.y >> 1) & 0x3) << 3) | (p.x & 0x7);
  const int outer_inner_bytes = (outer_blocks | inner_blocks)
                                << bytes_per_block_log2;
  const int bank = ((p.y >> 3) ^ (p.z >> 2)) & 0x1;
  const int pipe = ((p.x >> 3) & 0x3) ^ (bank << 1);
  return XenosTextureTiledAddressCombine(outer_inner_bytes, bank, pipe,
                                         p.y & 1);
}

// XOR to apply to the tiled address to flip the bits corresponding to the given
// X coordinate bits within the width of a macro tile.
// Note that in a tiled address, bit 7 is X[4] ^ Y[3] ^ Z[2], not X[4] alone.
int XenosTextureTiledAddressXInMacroXor(const int x,
                                        const uint bytes_per_block_log2) {
  return XenosTextureTiledAddressCombine((x & 0x7) << bytes_per_block_log2,
                                         0, (x >> 3) & 0x3, 0);
}

// The lowest bits of an block index within a micro tile are X[2:0].
// In a tiled address, the bit 4 is always Y[0].
// However, the bits [3:0] are the lower bits of the micro tile block index
// times the number of bytes per block.
// Because of this, a number of blocks, that depends on the count of bytes per
// block, along the X axis (aligned to this amount) is stored consecutively in
// guest memory:
// - 1bpb: 8 blocks (8 bytes - limited by address bit 3 being Y[1] for 1bpb).
// - 2bpb: 8 blocks (16 bytes - limited by address bit 4 always being Y[0]).
// - 4bpb: 4 blocks.
// - 8bpb: 2 blocks.
// - 16bpb: 1 block.
// This makes it possible to access multiple blocks in a single row using 8-byte
// or (for >= 2bpb) 16-byte loads and stores, and that's particularly useful
// when transferring texture data between tiled and linear storage.

// With resolution scaling, one scaled group of bytes in guest addresses
// corresponds to `scale.x * scale.y` groups of the same size on the host.
//
// A single group contains a full rectangular region of blocks. This means that,
// for instance, if the Y[1] tiled address bit is within the group size, Y[0]
// must be within it too, so division is enough to go from host to guest
// coordinates for the origin of the group.
//
// The address of the guest group on the host is the guest tiled address of its
// origin in guest coordinates multiplied by `scale.x * scale.y`.
//
// Within a guest group, the addressing of blocks is controlled by the host.
// Specifically, host groups are arranged in a guest group as group-linear
// column-major (for storage locality along both axes), and blocks in a host
// group are laid out as linear row-major (guest tiling therefore is applied
// only to whole guest groups, not within them, for simplicity).
//
// Addressing with resolution scaling is not intended to allow for
// reinterpretation of resolution-scaled data between different numbers of bytes
// per block. Rather, it's designed for simple and efficient access on the host,
// primarily when copying between tiled and linear storage, and to reduce the
// differences in shader logic between unscaled and scaled data.
//
// However, the groups are still small enough to preserve most of the tiling
// properties on a macro level, most importantly the possibility to resolve
// EDRAM render target regions to textures at different destination offsets.
//
// Also, the group sizes are selected to make resolution scaling calculations
// work the same for 2D and 3D textures, and also mostly position-independent -
// in particular, the bit 7 of a tiled address, which depends on Y[3] and Z[2],
// is never within the group size. This allows, for example, for downsampling of
// resolution-scaled data in a memory range to be done with the number of bytes
// per pixel being the only needed metadata.
//
// A common pattern in Xenia is copying multiple 8-byte or (for >= 2bpb) 16-byte
// sequences of consecutive blocks along the X axis in a single shader
// invocation, by computing the tiled address once and merely flipping X bits in
// it.
//
// With the resolution scaling group size being no larger than 2^7 bytes, it may
// contain guest X bits [3:0] for <= 4bpb, [1:0] for 8bpb, and [0] for 16bpb
// (note that though X[3] always goes to address[6], for 8bpb, X[2] is
// address[8], so a group can't be wider than 4 blocks, and similarly for X[1]
// for 16bpb).
//
// Given these requirements, the group sizes are chosen as follows:
// - 1bpb - lower 7 bits of an unscaled address are X0, X1, X2, Y1, Y0, Y2, X3:
//   - Group width: 2^4 blocks (maximum within 7 bits), or 2^4 bytes.
//   - Group height: 2^3 blocks (Y[2:0] between X[3:0]).
// - 2bpb - lower 7 bits of an unscaled address are 0, X0, X1, X2, Y0, Y1, X3:
//   - Group width: 2^4 blocks (maximum within 7 bits), or 2^5 bytes.
//   - Group height: 2^2 blocks (Y[1:0] between X[3:0]).
// - 4bpb - lower 7 bits of an unscaled address are 0, 0, X0, X1, Y0, X2, X3:
//   - Group width: 2^4 blocks (maximum within 7 bits), or 2^6 bytes.
//   - Group height: 2^1 blocks (Y[0] between X[3:0]).
// - 8bpb - lower 7 bits of an unscaled address are 0, 0, 0, X0, Y0, X1, X3:
//   - Group width: 2^2 blocks (X[2] is beyond 7 bits), or 2^5 bytes.
//   - Group height: 2^1 blocks (Y[0] between X[1:0]).
// - 16bpb - lower 7 bits of an unscaled address are 0, 0, 0, 0, Y0, X0, X3:
//   - Group width: 2^1 blocks (X[2:1] is beyond 7 bits), or 2^5 bytes.
//   - Group height: 2^1 blocks (Y[0] below X[0]).
//
// 2^6 bytes copied per invocation is likely to be optimal, as that consumes 16
// 32-bit VGPRs, out of a total of 24 (1024 / 40 rounded down to 4) available
// without VGPR usage becoming a theoretical occupancy limit on AMD GCN
// (although the occupancy of copy shaders is likely to be limited by memory
// accesses instead anyway).
//
// Note that with the given group sizes, as well as with blocks in a host group
// stored as row-major, for 1bpb, 16x1 host blocks are stored consecutively with
// resolution scaling (even though in guest tiling, only 8x1 blocks are), so
// they can be accessed via one 16-byte operation rather than two 8-byte ones.

// Expected to be called for a compile-time constant.
uint2_xe XeniaTextureResolutionScaledGroupBlocksLog2(
    const uint bytes_per_block_log2) {
  // Based on the tiled address properties, see the comment above for details.
  return uint2_xe(bytes_per_block_log2 >= 3u ? 5u - bytes_per_block_log2 : 4u,
                  3u - min(bytes_per_block_log2, 2u));
}

struct XeniaTextureResolutionScaledAddressing {
  uint2_xe guest_group_origin;
  uint host_byte_offset_in_guest_group;
};

XeniaTextureResolutionScaledAddressing
XeniaTextureGetResolutionScaledAddressing(const uint2_xe position,
                                          const uint2_xe resolution_scale,
                                          const uint bytes_per_block_log2) {
  XeniaTextureResolutionScaledAddressing addressing;

  const uint2_xe group_blocks_log2 =
      XeniaTextureResolutionScaledGroupBlocksLog2(bytes_per_block_log2);

  const uint2_xe host_group_id_in_texture = position >> group_blocks_log2;
  const uint2_xe guest_group_id_in_texture =
      host_group_id_in_texture / resolution_scale;
  const uint2_xe host_group_id_in_guest_group =
      host_group_id_in_texture - resolution_scale * guest_group_id_in_texture;

  addressing.guest_group_origin =
      guest_group_id_in_texture << group_blocks_log2;

  // Host groups are stored as column-major in a guest group, but this can be
  // changed freely.
  const uint host_group_index_in_guest_group =
      host_group_id_in_guest_group.x * resolution_scale.y +
      host_group_id_in_guest_group.y;
  // Shifts are expanded rather than chained because the number of bytes per
  // block, and thus also the group size, are expected to be compile-time
  // constants, so this is expected to be combined using GPU bitfield insert
  // instructions.
  const uint group_width_bytes_log2 =
      group_blocks_log2.x + bytes_per_block_log2;
  const uint2_xe position_in_host_group =
      position & ((uint_x2_xe(1u) << group_blocks_log2) - 1u);
  addressing.host_byte_offset_in_guest_group =
      (host_group_index_in_guest_group <<
           (group_width_bytes_log2 + group_blocks_log2.y)) |
      (position_in_host_group.y << group_width_bytes_log2) |
      (position_in_host_group.x << bytes_per_block_log2);

  return addressing;
}

int XeTextureHostLinearOffset(int3_xe p, uint pitch, uint height, uint bpb) {
  return p.x * int(bpb) + (p.z * int(height) + p.y) * int(pitch);
}

#endif  // XENIA_GPU_SHADERS_TEXTURE_ADDRESS_XESLI_
