/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2021-2026 Baldur Karlsson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/

#include "sparse_page_table.h"
#include "common/globalconfig.h"
#include "serialise/serialiser.h"

namespace Sparse
{
static uint64_t calcPageForTileCoord(const Coord &coord, const Coord &subresourcePageDim)
{
  return (((coord.z * subresourcePageDim.y) + coord.y) * subresourcePageDim.x) + coord.x;
}

void PageRangeMapping::createPages(uint64_t numPages, uint32_t pageByteSize)
{
  // don't do anything if the pages have already been populated
  if(!pages.empty())
    return;

  // this only truncates on 32-bit builds, where we don't expect large resources
  size_t count = (size_t)numPages;

  // otherwise allocate them. If we have a single page mapping we can just duplicate and it's easy
  if(singlePageReused || singleMapping.memory == ResourceId())
  {
    pages.fill(count, singleMapping);
  }
  else
  {
    // otherwise we need to allocate and increment
    pages.reserve(count);
    pages.clear();
    for(uint64_t i = 0; i < numPages; i++)
    {
      pages.push_back(singleMapping);
      singleMapping.offset += pageByteSize;
    }
  }

  // reset the single mapping to be super clear
  singleMapping = {};
  singlePageReused = false;
}

void PageTable::Initialise(uint64_t bufferByteSize, uint32_t pageByteSize)
{
  m_PageByteSize = pageByteSize;
  // set just in case the calling code calls getMipTailByteOffsetForSubresource
  m_ArraySize = 1;
  m_MipCount = 1;

  m_TextureDim = {bufferByteSize, 1, 1};
  m_PageTexelSize = {pageByteSize, 1, 1};

  // initialise mip tail with buffer properties
  m_MipTail.byteStride = 0;
  m_MipTail.byteOffset = 0;
  m_MipTail.firstMip = 0;
  m_MipTail.totalPackedByteSize = bufferByteSize;
  m_MipTail.mappings.resize(1);
  m_MipTail.mappings[0].singlePageReused = false;
  m_MipTail.mappings[0].singleMapping = {};
}

void PageTable::Initialise(const Coord &overallTexelDim, uint32_t numMips, uint32_t numArraySlices,
                           uint32_t pageByteSize, const Coord &pageTexelDim, uint32_t firstTailMip,
                           uint64_t mipTailOffset, uint64_t mipTailStride,
                           uint64_t mipTailTotalPackedSize)
{
  // sanitise inputs to be safe
  m_PageByteSize = RDCMAX(1U, pageByteSize);
  m_ArraySize = RDCMAX(1U, numArraySlices);
  m_MipCount = RDCMAX(1U, numMips);
  m_PageTexelSize.x = RDCMAX((uint64_t)1ULL, pageTexelDim.x);
  m_PageTexelSize.y = RDCMAX((uint64_t)1ULL, pageTexelDim.y);
  m_PageTexelSize.z = RDCMAX((uint64_t)1ULL, pageTexelDim.z);
  m_TextureDim.x = RDCMAX((uint64_t)1ULL, overallTexelDim.x);
  m_TextureDim.y = RDCMAX((uint64_t)1ULL, overallTexelDim.y);
  m_TextureDim.z = RDCMAX((uint64_t)1ULL, overallTexelDim.z);

  // initialise the subresources
  m_Subresources.resize(m_ArraySize * m_MipCount);

  // initialise mip tail if we have one
  if(firstTailMip < m_MipCount)
  {
    m_MipTail.byteStride = mipTailStride;
    m_MipTail.byteOffset = mipTailOffset;
    m_MipTail.firstMip = firstTailMip;
    m_MipTail.totalPackedByteSize = mipTailTotalPackedSize;

    if(m_MipTail.byteStride == 0)
    {
      m_MipTail.mappings.resize(1);
      m_MipTail.mappings[0].singlePageReused = false;
      m_MipTail.mappings[0].singleMapping = {};
    }
    else
    {
      m_MipTail.mappings.resize(m_ArraySize);
      for(size_t i = 0; i < m_MipTail.mappings.size(); i++)
      {
        m_MipTail.mappings[i].singlePageReused = false;
        m_MipTail.mappings[i].singleMapping = {};
      }
    }
  }
  else
  {
    m_MipTail.byteStride = 0;
    m_MipTail.byteOffset = 0;
    m_MipTail.firstMip = m_MipCount;
    m_MipTail.totalPackedByteSize = 0;
  }
}

uint64_t PageTable::setMipTailRange(uint64_t resourceByteOffset, ResourceId memory,
                                    uint64_t memoryByteOffset, uint64_t byteSize, bool useSinglePage)
{
  // offsets should be page aligned
  RDCASSERT((memoryByteOffset % m_PageByteSize) == 0, memoryByteOffset, m_PageByteSize);
  RDCASSERT((resourceByteOffset % m_PageByteSize) == 0, resourceByteOffset, m_PageByteSize);

  // size should either be page aligned, or should be the end of the mip tail region (for buffers
  // that don't have to fill the whole thing)
  RDCASSERT((byteSize % m_PageByteSize) == 0 ||
                (resourceByteOffset + byteSize == m_MipTail.totalPackedByteSize),
            resourceByteOffset, byteSize, m_PageByteSize, m_MipTail.totalPackedByteSize);

  RDCASSERT(m_MipTail.totalPackedByteSize > 0);

  // if we're setting to NULL, set the offset to 0
  if(memory == ResourceId())
    memoryByteOffset = 0;

  // rebase the byte offset from resource-relative to miptail-relative
  RDCASSERT(resourceByteOffset >= m_MipTail.byteOffset);
  resourceByteOffset -= m_MipTail.byteOffset;

  if(m_MipTail.mappings.empty())
  {
    RDCERR("Attempting to set mip tail on image with no mip region");
    return m_MipTail.byteOffset + m_MipTail.totalPackedByteSize;
  }

  const uint64_t numTailPages =
      ((m_MipTail.totalPackedByteSize + m_PageByteSize - 1) / m_PageByteSize);

  // if we're setting the whole mip tail at once, store it as a single page mapping
  if(resourceByteOffset == 0 &&
     (byteSize == m_MipTail.totalPackedByteSize || byteSize == numTailPages * m_PageByteSize))
  {
    for(size_t i = 0; i < m_MipTail.mappings.size(); i++)
    {
      m_MipTail.mappings[i].pages.clear();
      m_MipTail.mappings[i].singleMapping.memory = memory;
      m_MipTail.mappings[i].singleMapping.offset = memoryByteOffset;
      m_MipTail.mappings[i].singlePageReused = useSinglePage;

      // if we're not using a single page and we have multiple mip tails separated by a stride,
      // update the memory offset for each single mapping. This implies wasted memory in between so
      // I don't think apps will do this, but it may be legal so handle it here.
      if(!useSinglePage)
        memoryByteOffset += i * m_MipTail.byteStride;
    }

    // we consumed the whole mip tail by definition
    return m_MipTail.byteOffset + m_MipTail.totalPackedByteSize;
  }
  else if(m_MipTail.mappings.size() == 1)
  {
    // if we only have one miptail region, this is simple. Create pages as needed and update the
    // referenced pages
    PageRangeMapping &mapping = m_MipTail.mappings[0];

    mapping.createPages(numTailPages, m_PageByteSize);

    // iterate through each referenced resource page
    for(size_t
            page = size_t(resourceByteOffset / m_PageByteSize),
            endPage = size_t((resourceByteOffset + byteSize + m_PageByteSize - 1) / m_PageByteSize);
        page < mapping.pages.size() && page < endPage; page++)
    {
      mapping.pages[page].memory = memory;
      mapping.pages[page].offset = memoryByteOffset;

      // if we're not mapping all resource pages to a single memory page, advance the offset
      if(!useSinglePage && memory != ResourceId())
        memoryByteOffset += m_PageByteSize;
    }

    mapping.simplifyUnmapped();

    // return how much of the mip tail we consumed, clamped to the size. Note resourceByteOffset has
    // been remapped to be mip-tail relative here
    return m_MipTail.byteOffset +
           RDCMIN(m_MipTail.totalPackedByteSize, resourceByteOffset + byteSize);
  }
  else
  {
    // otherwise the hard case - separate mip tails for each subresource. Figure out which
    // subresource we're starting with
    RDCASSERTNOTEQUAL(m_MipTail.byteStride, 0);
    uint32_t sub = uint32_t(resourceByteOffset / m_MipTail.byteStride);
    resourceByteOffset -= sub * m_MipTail.byteStride;

    const uint64_t mipTailSubresourceByteSize =
        m_MipTail.totalPackedByteSize / m_MipTail.mappings.size();

    // while we have mapping bytes to consume and the subresource is in range
    while(byteSize > 0 && sub < m_MipTail.mappings.size())
    {
      PageRangeMapping &mapping = m_MipTail.mappings[sub];

      uint64_t consumedBytes = 0;

      // if we're setting the whole miptail, store that concisely
      if(resourceByteOffset == 0 && byteSize >= mipTailSubresourceByteSize)
      {
        mapping.pages.clear();
        mapping.singleMapping.memory = memory;
        mapping.singleMapping.offset = memoryByteOffset;
        mapping.singlePageReused = useSinglePage;

        if(!useSinglePage)
          memoryByteOffset += m_MipTail.byteStride;

        consumedBytes = mipTailSubresourceByteSize;
      }
      else
      {
        mapping.createPages(((mipTailSubresourceByteSize + m_PageByteSize - 1) / m_PageByteSize),
                            m_PageByteSize);

        // iterate through each referenced page in this subresource's mip tail. Note we only iterate
        // over as many pages as this mapping has, even if the bound region is larger.
        for(size_t page = size_t(resourceByteOffset / m_PageByteSize),
                   endPage =
                       size_t((resourceByteOffset + byteSize + m_PageByteSize - 1) / m_PageByteSize);
            page < mapping.pages.size() && page < endPage; page++)
        {
          mapping.pages[page].memory = memory;
          mapping.pages[page].offset = memoryByteOffset;

          // if we're not mapping all resource pages to a single memory page, advance the offset
          if(!useSinglePage && memory != ResourceId())
            memoryByteOffset += m_PageByteSize;

          consumedBytes += m_PageByteSize;
        }

        memoryByteOffset += m_MipTail.byteStride - mipTailSubresourceByteSize;
      }

      // if we have fully set this mip tail, move to the next subresource's mip tail.
      // note this covers the case where we set exactly all the bytes in the mip tail, where we set
      // more bytes than exist but don't overlap into the next (based on stride), as well as the
      // case where we have bytes to set in the next subresource too. In the first two cases we will
      // just return, but in the last case we 'consume' the stride and get ready to continue
      if(resourceByteOffset + consumedBytes >= mipTailSubresourceByteSize)
      {
        // we start from the first byte in the next miptail
        resourceByteOffset = 0;

        // advance over the consumed bytes
        byteSize -= consumedBytes;

        // advance over the padding bytes.
        // if we don't have enough remaining to hit the stride, we just zero-out the number of bytes
        // remaining
        byteSize -= RDCMIN(byteSize, m_MipTail.byteStride - mipTailSubresourceByteSize);

        // simplify current mapping before moving on to next subresource
        mapping.simplifyUnmapped();
        sub++;
      }
      else
      {
        // and consume all bytes, even if that is more than we actually used above (consider if the
        // user specifies more than in the tail, but less than the stride)
        byteSize = 0;

        resourceByteOffset += consumedBytes;
      }
    }

    // simplify the last tail that we touched, as long as it's still valid. If we just incremented
    // past the last, we will have simplified above where sub is incremented
    if(sub < m_MipTail.mappings.size())
      m_MipTail.mappings[sub].simplifyUnmapped();

    if(byteSize > 0)
      RDCERR("Unclaimed bytes being assigned to image after iterating over all subresources");

    return m_MipTail.byteOffset + sub * m_MipTail.byteStride + resourceByteOffset;
  }
}

void PageTable::setImageBoxRange(uint32_t subresource, const Coord &coord, const Coord &dim,
                                 ResourceId memory, uint64_t memoryByteOffset, bool useSinglePage)
{
  const Coord subresourcePageDim = calcSubresourcePageDim(subresource);
  const Coord subresourceImgDim = getSubresourceDim(subresource);

  RDCASSERT((coord.x % m_PageTexelSize.x) == 0);
  RDCASSERT((coord.y % m_PageTexelSize.y) == 0);
  RDCASSERT((coord.z % m_PageTexelSize.z) == 0);

  // dimension may be misaligned if it's referring to part of a page on a non-page-aligned texture
  // dimension
  RDCASSERT((dim.x % m_PageTexelSize.x) == 0 || (coord.x + dim.x == subresourceImgDim.x),
            subresource, dim.x, coord.x, m_PageTexelSize.x, m_TextureDim.x, subresourceImgDim.x);
  RDCASSERT((dim.y % m_PageTexelSize.y) == 0 || (coord.y + dim.y == subresourceImgDim.y),
            subresource, dim.y, coord.y, m_PageTexelSize.y, m_TextureDim.y, subresourceImgDim.y);
  RDCASSERT((dim.z % m_PageTexelSize.z) == 0 || (coord.z + dim.z == subresourceImgDim.z),
            subresource, dim.z, coord.z, m_PageTexelSize.z, m_TextureDim.z, subresourceImgDim.z);

  // convert coords and dim to pages for ease of calculation
  Coord curCoord = coord;
  curCoord.x /= m_PageTexelSize.x;
  curCoord.y /= m_PageTexelSize.y;
  curCoord.z /= m_PageTexelSize.z;

  Coord curDim = dim;
  curDim.x = RDCMAX((uint64_t)1ULL, (curDim.x + m_PageTexelSize.x - 1) / m_PageTexelSize.x);
  curDim.y = RDCMAX((uint64_t)1ULL, (curDim.y + m_PageTexelSize.y - 1) / m_PageTexelSize.y);
  curDim.z = RDCMAX((uint64_t)1ULL, (curDim.z + m_PageTexelSize.z - 1) / m_PageTexelSize.z);

  RDCASSERT(subresource < m_Subresources.size(), subresource, m_Subresources.size());
  RDCASSERT(curCoord.x < subresourcePageDim.x && curCoord.y < subresourcePageDim.y &&
            curCoord.z < subresourcePageDim.z);
  RDCASSERT(curCoord.x + curDim.x <= subresourcePageDim.x &&
            curCoord.y + curDim.y <= subresourcePageDim.y &&
            curCoord.z + curDim.z <= subresourcePageDim.z);

  // if we're setting to NULL, set the offset to 0
  if(memory == ResourceId())
    memoryByteOffset = 0;

  PageRangeMapping &sub = m_Subresources[subresource];

  // if we're setting the whole subresource, set it to use the optimal single mapping
  if(curCoord.x == 0 && curCoord.y == 0 && curCoord.z == 0 && curDim == subresourcePageDim)
  {
    sub.pages.clear();
    sub.singleMapping.memory = memory;
    sub.singleMapping.offset = memoryByteOffset;
    sub.singlePageReused = useSinglePage;
  }
  else
  {
    // either we're starting at a coord somewhere into the subresource, or we don't cover it all.
    // Split the subresource into pages if needed and update.
    const uint64_t numSubresourcePages =
        (subresourcePageDim.x * subresourcePageDim.y * subresourcePageDim.z);
    sub.createPages(numSubresourcePages, m_PageByteSize);

    for(uint64_t z = curCoord.z; z < curCoord.z + curDim.z; z++)
    {
      for(uint64_t y = curCoord.y; y < curCoord.y + curDim.y; y++)
      {
        for(uint64_t x = curCoord.x; x < curCoord.x + curDim.x; x++)
        {
          // calculate the page
          const uint64_t page = calcPageForTileCoord({x, y, z}, subresourcePageDim);

          sub.accessPage(page).memory = memory;
          sub.accessPage(page).offset = memoryByteOffset;

          // if we're not mapping all resource pages to a single memory page, advance the offset
          if(!useSinglePage && memory != ResourceId())
            memoryByteOffset += m_PageByteSize;
        }
      }
    }

    sub.simplifyUnmapped();
  }
}

rdcpair<uint32_t, Coord> PageTable::setImageWrappedRange(uint32_t subresource,
                                                         const Sparse::Coord &coord,
                                                         uint64_t byteSize, ResourceId memory,
                                                         uint64_t memoryByteOffset,
                                                         bool useSinglePage, bool updateMappings)
{
  const bool allBytes = (byteSize == ~0U);

  if(allBytes)
    byteSize -= byteSize % m_PageByteSize;

  RDCASSERT((byteSize % m_PageByteSize) == 0, byteSize, m_PageByteSize);

  RDCASSERT(subresource < m_Subresources.size() || isSubresourceInMipTail(subresource), subresource,
            m_Subresources.size());

  Sparse::Coord curCoord = coord;

  // if we're setting to NULL, set the offset to 0
  if(memory == ResourceId())
    memoryByteOffset = 0;

  // loop while we still have bytes left, to allow wrapping over subresources
  while(byteSize > 0 && (isSubresourceInMipTail(subresource) || subresource < m_Subresources.size()))
  {
    const Coord subresourcePageDim = calcSubresourcePageDim(subresource);
    const uint64_t numMipTailPages =
        uint64_t((m_MipTail.totalPackedByteSize / RDCMAX(1U, (uint32_t)m_MipTail.mappings.size())) +
                 m_PageByteSize - 1) /
        m_PageByteSize;
    const uint64_t numSubresourcePages =
        isSubresourceInMipTail(subresource)
            ? numMipTailPages
            : subresourcePageDim.x * subresourcePageDim.y * subresourcePageDim.z;

    PageRangeMapping &sub = isSubresourceInMipTail(subresource) ? getMipTailMapping(subresource)
                                                                : m_Subresources[subresource];

    const uint64_t subresourceByteSize = numSubresourcePages * m_PageByteSize;

    // if we're setting the whole subresource, set it to use the optimal single mapping
    if(curCoord.x == 0 && curCoord.y == 0 && curCoord.z == 0 && byteSize >= subresourceByteSize)
    {
      if(updateMappings)
      {
        sub.pages.clear();
        sub.singleMapping.memory = memory;
        sub.singleMapping.offset = memoryByteOffset;
        sub.singlePageReused = useSinglePage;
      }

      // if we're not mapping all resource pages to a single memory page, advance the offset
      if(!useSinglePage && memory != ResourceId())
        memoryByteOffset += numSubresourcePages * m_PageByteSize;

      byteSize -= subresourceByteSize;

      // continue on the next subresource at {0,0,0}. If there are bytes remaining we'll loop and
      // assign them

      // if we're setting in the miptail right now, continue on at the next mip 0
      if(isSubresourceInMipTail(subresource))
      {
        const uint32_t slice = subresource / m_MipCount;
        subresource = (slice + 1) * m_MipCount;
      }
      else
      {
        subresource++;
        curCoord = {0, 0, 0};
      }

      // since we know we consumed a whole subresource above, if we're done then we can return the
      // correct reference to the next subresource at 0,0,0 here
      if(byteSize == 0)
        return {subresource, curCoord};
    }
    else
    {
      // either we're starting at a coord somewhere into the subresource, or we don't cover it all.
      // Split the subresource into pages if needed and update.
      if(updateMappings)
        sub.createPages(numSubresourcePages, m_PageByteSize);

      // note that numPages could be more pages than in the subresource! hence below we check both
      // that we haven't exhausted the incoming pages and that we haven't exhausted the pages in the
      // subresource
      const uint64_t numPages = byteSize / m_PageByteSize;

      if(isSubresourceInMipTail(subresource))
      {
        curCoord.x /= m_PageTexelSize.x;
        curCoord.y = 0;
        curCoord.z = 0;
      }
      else
      {
        // convert current co-ord to pages for calculation. We don't have to worry about doing this
        // repeatedly because if we overlap into another subresource we start at 0,0,0
        curCoord.x /= m_PageTexelSize.x;
        curCoord.y /= m_PageTexelSize.y;
        curCoord.z /= m_PageTexelSize.z;
      }

      // calculate the starting page
      uint64_t startingPage =
          (((curCoord.z * subresourcePageDim.y) + curCoord.y) * subresourcePageDim.x) + curCoord.x;

      for(uint64_t page = startingPage;
          page < startingPage + numPages && page < numSubresourcePages; page++)
      {
        if(updateMappings)
        {
          sub.accessPage(page).memory = memory;
          sub.accessPage(page).offset = memoryByteOffset;
        }

        // if we're not mapping all resource pages to a single memory page, advance the offset
        if(!useSinglePage && memory != ResourceId())
          memoryByteOffset += m_PageByteSize;
        byteSize -= m_PageByteSize;
      }

      if(updateMappings)
        sub.simplifyUnmapped();

      // if we consumed all bytes and didn't get to the end of the subresource, calculate where we
      // ended up
      if(byteSize == 0 && startingPage + numPages < numSubresourcePages)
      {
        if(isSubresourceInMipTail(subresource))
        {
          curCoord.x = (curCoord.x + numPages) * m_PageTexelSize.x;
        }
        else
        {
          // X we just increment by however many pages, wrapping by the row length in pages
          curCoord.x = (curCoord.x + numPages) % subresourcePageDim.x;
          // for Y we increment by however many *rows*, again wrapping
          curCoord.y = (curCoord.y + numPages / subresourcePageDim.x) % subresourcePageDim.y;
          // similarly for Z
          curCoord.z = (curCoord.z + numPages / (subresourcePageDim.x * subresourcePageDim.y)) %
                       subresourcePageDim.z;
        }

        return {subresource, curCoord};
      }

      // continue on the next subresource at {0,0,0}. If there are bytes remaining we'll loop and
      // assign them

      // if we're setting in the miptail right now, continue on at the next mip 0
      if(isSubresourceInMipTail(subresource))
      {
        const uint32_t slice = subresource / m_MipCount;
        subresource = (slice + 1) * m_MipCount;
        curCoord = {0, 0, 0};
      }
      else
      {
        subresource++;
        curCoord = {0, 0, 0};
      }

      // if we got here we consumed the whole subresource and ended, so return that
      if(byteSize == 0)
        return {subresource, curCoord};
    }
  }

  if(!allBytes && byteSize > 0)
    RDCERR("Unclaimed bytes being assigned to image after iterating over all subresources");

  return {0, {0, 0, 0}};
}

void PageTable::copyImageBoxRange(uint32_t dstSubresource, const Coord &coordInTiles,
                                  const Coord &dimInTiles, const PageTable &srcPageTable,
                                  uint32_t srcSubresource, const Coord &srcCoordInTiles)
{
  // this can only be used if pages are the same size, which is always true on D3D
  RDCASSERT(getPageByteSize() == srcPageTable.getPageByteSize());

  const bool srcMip = srcPageTable.isSubresourceInMipTail(srcSubresource);
  const bool dstMip = isSubresourceInMipTail(dstSubresource);

  // if we're copying between mip tails and the copy is completely contained in the mip tail on
  // both sides of the copy
  if(srcMip && dstMip &&
     dimInTiles.x <= srcPageTable.getMipTailSliceSize() / srcPageTable.getPageByteSize() &&
     dimInTiles.x <= getMipTailSliceSize() / getPageByteSize())
  {
    const Sparse::PageRangeMapping &srcMapping = srcPageTable.getMipTailMapping(srcSubresource);

    // if the source has a single mapping, copy it and allow setMipTailRange to figure out if this
    // is a single mapping in the destination or not
    if(srcMapping.hasSingleMapping())
    {
      setMipTailRange(
          getMipTailByteOffsetForSubresource(dstSubresource) + coordInTiles.x * m_PageByteSize,
          srcMapping.singleMapping.memory,
          srcMapping.singleMapping.offset + srcCoordInTiles.x * m_PageByteSize,
          dimInTiles.x * m_PageByteSize, srcMapping.singlePageReused);
    }
    // otherwise we just do a page-by-page copy
    else
    {
      uint64_t dstStartPage = coordInTiles.x;
      uint64_t srcStartPage = srcCoordInTiles.x;
      for(uint64_t pageIdx = 0; pageIdx < dimInTiles.x; pageIdx++)
      {
        Page srcPageContents = srcMapping.getPage(srcStartPage + pageIdx, m_PageByteSize);
        setMipTailRange(dstStartPage * m_PageByteSize, srcPageContents.memory,
                        srcPageContents.offset, m_PageByteSize, false);
      }
    }

    return;
  }

  Sparse::Coord srcSubSize = srcPageTable.calcSubresourcePageDim(srcSubresource);
  Sparse::Coord dstSubSize = calcSubresourcePageDim(dstSubresource);

  // if the region covers the destination subresource we might be able to take a shortcut
  if(dstSubSize == dimInTiles)
  {
    // if the subresources are identical, just copy no matter what
    if(srcSubSize == dstSubSize)
    {
      m_Subresources[dstSubresource] = srcPageTable.getSubresource(srcSubresource);
      return;
    }

    // one more case - if the destination is completely covered by the region and the source is at
    // least as big AND the source is a single page mapping, we can copy to a single page mapping.
    // That's because the pages are the same they just wrap differently (e.g. in an original 8x8
    // source the pages 0..7 form the first row and in the destination 4x4 those same pages are the
    // first two rows, but that's exactly what this operation wants to do).
    if(srcSubSize.x >= dimInTiles.x && srcSubSize.y >= dimInTiles.y && srcSubSize.z >= dimInTiles.z &&
       srcPageTable.getSubresource(srcSubresource).hasSingleMapping())
    {
      m_Subresources[dstSubresource] = srcPageTable.getSubresource(srcSubresource);
      return;
    }
  }

  // in all other cases we fall back to a page-by-page copy from source to dest. This case means the
  // box is a subset of the destination or the source isn't a nice single mapping so we need to copy
  // pages one by one

  // box regions are by definition constrained within a subresource. We assume it's
  // constrained within both...
  RDCASSERT(srcCoordInTiles.x + dimInTiles.x <= srcSubSize.x, srcCoordInTiles.x, dimInTiles.x,
            srcSubSize.x);
  RDCASSERT(srcCoordInTiles.y + dimInTiles.y <= srcSubSize.y, srcCoordInTiles.y, dimInTiles.x,
            srcSubSize.y);
  RDCASSERT(srcCoordInTiles.z + dimInTiles.z <= srcSubSize.z, srcCoordInTiles.z, dimInTiles.x,
            srcSubSize.z);

  RDCASSERT(coordInTiles.x + dimInTiles.x <= dstSubSize.x, coordInTiles.x, dimInTiles.x,
            dstSubSize.x);
  RDCASSERT(coordInTiles.y + dimInTiles.y <= dstSubSize.y, coordInTiles.y, dimInTiles.y,
            dstSubSize.y);
  RDCASSERT(coordInTiles.z + dimInTiles.z <= dstSubSize.z, coordInTiles.z, dimInTiles.z,
            dstSubSize.z);

  Sparse::PageRangeMapping &dstSub = m_Subresources[dstSubresource];
  const Sparse::PageRangeMapping &srcSub = srcPageTable.getSubresource(srcSubresource);

  dstSub.createPages(dstSubSize.x * dstSubSize.y * dstSubSize.z, m_PageByteSize);

  for(uint64_t z = 0; z < dimInTiles.z; z++)
  {
    for(uint64_t y = 0; y < dimInTiles.y; y++)
    {
      for(uint64_t x = 0; x < dimInTiles.x; x++)
      {
        const uint64_t dstPage = calcPageForTileCoord(
            {coordInTiles.x + x, coordInTiles.y + y, coordInTiles.z + z}, dstSubSize);
        const uint64_t srcPage = calcPageForTileCoord(
            {srcCoordInTiles.x + x, srcCoordInTiles.y + y, srcCoordInTiles.z + z}, srcSubSize);
        dstSub.accessPage(dstPage) = srcSub.getPage(srcPage, m_PageByteSize);
      }
    }
  }

  dstSub.simplifyUnmapped();
}

void PageTable::copyImageWrappedRange(uint32_t dstSubresource, const Coord &coordInTiles,
                                      uint64_t numTiles, const PageTable &srcPageTable,
                                      uint32_t srcSubresource, const Coord &srcCoordInTiles)
{
  // this can only be used if pages are the same size, which is always true on D3D
  RDCASSERT(getPageByteSize() == srcPageTable.getPageByteSize());

  const bool srcMip = srcPageTable.isSubresourceInMipTail(srcSubresource);
  const bool dstMip = isSubresourceInMipTail(dstSubresource);

  // if we're copying between mip tails and the copy is completely contained in the mip tail on
  // both sides of the copy
  if(srcMip && dstMip &&
     numTiles <= srcPageTable.getMipTailSliceSize() / srcPageTable.getPageByteSize() &&
     numTiles <= getMipTailSliceSize() / getPageByteSize())
  {
    const Sparse::PageRangeMapping &srcMapping = srcPageTable.getMipTailMapping(srcSubresource);

    // if the source has a single mapping, copy it and allow setMipTailRange to figure out if this
    // is a single mapping in the destination or not
    if(srcMapping.hasSingleMapping())
    {
      setMipTailRange(
          getMipTailByteOffsetForSubresource(dstSubresource) + coordInTiles.x * m_PageByteSize,
          srcMapping.singleMapping.memory,
          srcMapping.singleMapping.offset + srcCoordInTiles.x * m_PageByteSize,
          numTiles * m_PageByteSize, srcMapping.singlePageReused);
    }
    // otherwise we just do a page-by-page copy
    else
    {
      uint64_t dstStartPage = coordInTiles.x;
      uint64_t srcStartPage = srcCoordInTiles.x;
      for(uint64_t pageIdx = 0; pageIdx < numTiles; pageIdx++)
      {
        Page srcPageContents = srcMapping.getPage(srcStartPage + pageIdx, m_PageByteSize);
        setMipTailRange(dstStartPage * m_PageByteSize, srcPageContents.memory,
                        srcPageContents.offset, m_PageByteSize, false);
      }
    }

    return;
  }

  // in all other cases we fall back to a page-by-page copy from source to dest, overlapping pages
  // linearly.
  Sparse::Coord srcSubSize = srcPageTable.calcSubresourcePageDim(srcSubresource);
  Sparse::Coord dstSubSize = calcSubresourcePageDim(dstSubresource);

  const Sparse::PageRangeMapping *srcMapping = &srcPageTable.getSubresource(srcSubresource);
  Sparse::PageRangeMapping *dstMapping = &m_Subresources[dstSubresource];

  uint64_t dstPage =
      calcPageForTileCoord({coordInTiles.x, coordInTiles.y, coordInTiles.z}, dstSubSize);
  uint64_t srcPage =
      calcPageForTileCoord({srcCoordInTiles.x, srcCoordInTiles.y, srcCoordInTiles.z}, srcSubSize);

  for(uint64_t i = 0; i < numTiles;)
  {
    const uint64_t remainingTiles = numTiles - i;

    const uint64_t dstSubTiles = (dstSubSize.x * dstSubSize.y * dstSubSize.z);
    const uint64_t srcSubTiles = (srcSubSize.x * srcSubSize.y * srcSubSize.z);

    // if we currently have enough tiles remaining to update the whole next resource and we're
    // pointing at 0,0,0 and our source range is a single mapping, copy that single mapping.
    if(dstPage == 0 && dstSubTiles <= remainingTiles && srcMapping->hasSingleMapping() &&
       dstSubTiles <= srcSubTiles - srcPage)
    {
      // as above, the tiles may wrap differently in the destination mapping but that's how we want
      // it to be
      *dstMapping = *srcMapping;

      // increment by how many tiles we consumed
      srcPage += dstSubTiles;
      dstPage += dstSubTiles;
      i += dstSubTiles;
    }
    else
    {
      // otherwise just copy the current page
      dstMapping->createPages(dstSubTiles, m_PageByteSize);
      dstMapping->accessPage(dstPage) = srcMapping->getPage(srcPage, m_PageByteSize);

      dstPage++;
      srcPage++;
      i++;
    }

    if(i >= numTiles)
      break;

    // wrap the destination and source to the next page in the next subresource if we've used all
    // the ones up
    if(srcPage >= srcSubTiles)
    {
      // start at page 0 in the next subresource
      srcPage = 0;

      // if we were in the mip tail, jump all the way to the next mip0, otherwise we just move to
      // the next subresource
      if(isSubresourceInMipTail(srcSubresource))
      {
        const uint32_t slice = srcSubresource / srcPageTable.getMipCount();
        srcSubresource = (slice + 1) * srcPageTable.getMipCount();
      }
      else
      {
        srcSubresource++;
      }

      // update subresource properties for the new current subresource
      srcSubSize = srcPageTable.calcSubresourcePageDim(srcSubresource);
      srcMapping = &srcPageTable.getSubresource(srcSubresource);
    }

    if(dstPage >= dstSubTiles)
    {
      // start at page 0 in the next subresource
      dstPage = 0;

      // if we were in the mip tail, jump all the way to the next mip0, otherwise we just move to
      // the next subresource
      if(isSubresourceInMipTail(dstSubresource))
      {
        const uint32_t slice = dstSubresource / m_MipCount;
        dstSubresource = (slice + 1) * m_MipCount;
      }
      else
      {
        dstSubresource++;
      }

      // simplify the mapping before we move on
      dstMapping->simplifyUnmapped();

      dstSubSize = calcSubresourcePageDim(dstSubresource);
      dstMapping = &m_Subresources[dstSubresource];
    }

    if(srcSubresource >= srcPageTable.getNumSubresources())
    {
      dstMapping->simplifyUnmapped();

      RDCERR(
          "Number of tiles %u in image wrapped range copy exceeded source page table subresource "
          "count %u",
          numTiles, srcPageTable.getNumSubresources());
      return;
    }

    if(dstSubresource >= getNumSubresources())
    {
      dstMapping->simplifyUnmapped();

      RDCERR(
          "Number of tiles %u in image wrapped range copy exceeded dest page table subresource "
          "count %u",
          numTiles, getNumSubresources());
      return;
    }
  }

  // simplify the last mapping we were working on
  if(dstMapping < m_Subresources.end())
    dstMapping->simplifyUnmapped();
}

const Coord PageTable::getSubresourceDim(uint32_t subresource) const
{
  const uint32_t mipLevel = subresource % m_MipCount;

  return {
      RDCMAX((uint64_t)1ULL, m_TextureDim.x >> mipLevel),
      RDCMAX((uint64_t)1ULL, m_TextureDim.y >> mipLevel),
      RDCMAX((uint64_t)1ULL, m_TextureDim.z >> mipLevel),
  };
}

Coord PageTable::calcSubresourcePageDim(uint32_t subresource) const
{
  const Coord mipDim = getSubresourceDim(subresource);

  // for each page that is fully or partially used
  return {RDCMAX((uint64_t)1ULL, (mipDim.x + m_PageTexelSize.x - 1) / m_PageTexelSize.x),
          RDCMAX((uint64_t)1ULL, (mipDim.y + m_PageTexelSize.y - 1) / m_PageTexelSize.y),
          RDCMAX((uint64_t)1ULL, (mipDim.z + m_PageTexelSize.z - 1) / m_PageTexelSize.z)};
}

uint64_t PageTable::GetSerialiseSize() const
{
  uint64_t ret = 0;

  // size of the pair itself
  ret += sizeof(*this);

  // for each mip tail region
  for(uint32_t s = 0; s < getMipTail().mappings.size(); s++)
  {
    ret += sizeof(Sparse::PageRangeMapping);

    // and the size of each page if the range mapping is expanded
    if(!getMipTail().mappings[s].hasSingleMapping())
      ret += sizeof(Sparse::Page) * getMipTail().mappings[s].pages.size();
  }

  // for each subresource the size of it
  for(uint32_t s = 0; s < getNumSubresources(); s++)
  {
    ret += sizeof(Sparse::PageRangeMapping);

    // and the size of each page if the range mapping is expanded
    if(!getSubresource(s).hasSingleMapping())
      ret += sizeof(Sparse::Page) * getSubresource(s).pages.size();
  }

  return ret;
}
};    // namespace Sparse

template <typename SerialiserType>
void DoSerialise(SerialiserType &ser, Sparse::Coord &el)
{
  if(ser.VersionAtLeast(0x20))
  {
    SERIALISE_MEMBER(x);
    SERIALISE_MEMBER(y);
    SERIALISE_MEMBER(z);
  }
  else
  {
    // older co-ords were 32-bit.
    RDCASSERT(ser.IsReading());
    SERIALISE_ELEMENT_LOCAL(x, uint32_t(0));
    SERIALISE_ELEMENT_LOCAL(y, uint32_t(0));
    SERIALISE_ELEMENT_LOCAL(z, uint32_t(0));

    el.x = x;
    el.y = y;
    el.z = z;
  }
}

template <typename SerialiserType>
void DoSerialise(SerialiserType &ser, Sparse::Page &el)
{
  SERIALISE_MEMBER(memory);
  SERIALISE_MEMBER(offset).OffsetOrSize();
}

template <typename SerialiserType>
void DoSerialise(SerialiserType &ser, Sparse::PageRangeMapping &el)
{
  SERIALISE_MEMBER(singleMapping);
  SERIALISE_MEMBER(pages);
}

template <typename SerialiserType>
void DoSerialise(SerialiserType &ser, Sparse::MipTail &el)
{
  SERIALISE_MEMBER(firstMip);
  SERIALISE_MEMBER(byteOffset).OffsetOrSize();
  SERIALISE_MEMBER(byteStride).OffsetOrSize();
  SERIALISE_MEMBER(totalPackedByteSize).OffsetOrSize();
  SERIALISE_MEMBER(mappings);
}

template <typename SerialiserType>
void DoSerialise(SerialiserType &ser, Sparse::PageTable &el)
{
  SERIALISE_MEMBER(m_TextureDim);
  SERIALISE_MEMBER(m_MipCount);
  SERIALISE_MEMBER(m_ArraySize);
  SERIALISE_MEMBER(m_PageByteSize).OffsetOrSize();
  SERIALISE_MEMBER(m_PageTexelSize);
  SERIALISE_MEMBER(m_Subresources);
  SERIALISE_MEMBER(m_MipTail);
}

INSTANTIATE_SERIALISE_TYPE(Sparse::Coord);
INSTANTIATE_SERIALISE_TYPE(Sparse::Page);
INSTANTIATE_SERIALISE_TYPE(Sparse::PageRangeMapping);
INSTANTIATE_SERIALISE_TYPE(Sparse::MipTail);
INSTANTIATE_SERIALISE_TYPE(Sparse::PageTable);

#if ENABLED(ENABLE_UNIT_TESTS)

#include "catch/catch.hpp"

template <>
rdcstr DoStringise(const Sparse::Coord32 &el)
{
  return StringFormat::Fmt("{%u, %u, %u}", el.x, el.y, el.z);
}

template <>
rdcstr DoStringise(const Sparse::Coord &el)
{
  return StringFormat::Fmt("{%llu, %llu, %llu}", el.x, el.y, el.z);
}

template <>
rdcstr DoStringise(const Sparse::Page &el)
{
  return StringFormat::Fmt("%s:%llu", ToStr(el.memory).c_str(), el.offset);
}

TEST_CASE("Test sparse page table mapping", "[sparse]")
{
  Sparse::PageTable pageTable;

  SECTION("normal buffer")
  {
    pageTable.Initialise(256, 64);

    CHECK(pageTable.getPageByteSize() == 64);
    CHECK(pageTable.getMipTail().byteOffset == 0);
    CHECK(pageTable.getMipTail().byteStride == 0);
    CHECK(pageTable.getMipTail().totalPackedByteSize == 256);
    CHECK(pageTable.getMipTail().firstMip == 0);
    REQUIRE(pageTable.getMipTail().mappings.size() == 1);
    CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({ResourceId(), 0}));

    uint64_t nextTailOffset;

    SECTION("Set all pages")
    {
      ResourceId mem = ResourceIDGen::GetNewUniqueID();

      nextTailOffset = pageTable.setBufferRange(0, mem, 512, 256, false);

      CHECK(nextTailOffset == 256);

      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mem, 512}));
      CHECK_FALSE(pageTable.getMipTail().mappings[0].singlePageReused);
    };

    SECTION("Set repeated page")
    {
      ResourceId mem = ResourceIDGen::GetNewUniqueID();

      nextTailOffset = pageTable.setBufferRange(0, mem, 512, 256, true);

      CHECK(nextTailOffset == 256);

      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mem, 512}));
      CHECK(pageTable.getMipTail().mappings[0].singlePageReused);
    };

    SECTION("Set page subsets")
    {
      ResourceId mem = ResourceIDGen::GetNewUniqueID();

      nextTailOffset = pageTable.setBufferRange(128, mem, 512, 64, false);

      CHECK(nextTailOffset == 128 + 64);

      CHECK_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 4);
      CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem, 512}));
      CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({ResourceId(), 0}));

      nextTailOffset = pageTable.setBufferRange(0, mem, 1024, 64, false);

      CHECK(nextTailOffset == 64);

      CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem, 1024}));
      CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem, 512}));
      CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({ResourceId(), 0}));

      nextTailOffset = pageTable.setBufferRange(64, mem, 128, 128, false);

      CHECK(nextTailOffset == 64 + 128);

      CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem, 1024}));
      CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem, 128}));
      CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem, 128 + 64}));
      CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({ResourceId(), 0}));

      nextTailOffset = pageTable.setBufferRange(64, mem, 256, 128, true);

      CHECK(nextTailOffset == 64 + 128);

      CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem, 1024}));
      CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem, 256}));
      CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem, 256}));
      CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({ResourceId(), 0}));

      nextTailOffset = pageTable.setBufferRange(64, ResourceId(), 256, 64, true);

      CHECK(nextTailOffset == 64 + 64);

      CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem, 1024}));
      CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem, 256}));
      CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({ResourceId(), 0}));
    };

    SECTION("Setting with wrapped incrementally")
    {
      ResourceId mem = ResourceIDGen::GetNewUniqueID();

      rdcpair<uint32_t, Sparse::Coord> curCoord = {0, {0, 0, 0}};
      curCoord = pageTable.setImageWrappedRange(curCoord.first, curCoord.second, 64, mem, 1024,
                                                false, true);

      CHECK(curCoord.first == 0);
      CHECK(curCoord.second == Sparse::Coord({64, 0, 0}));

      curCoord = pageTable.setImageWrappedRange(curCoord.first, curCoord.second, 64, mem, 1024,
                                                false, true);

      CHECK(curCoord.first == 0);
      CHECK(curCoord.second == Sparse::Coord({128, 0, 0}));

      curCoord = pageTable.setImageWrappedRange(curCoord.first, curCoord.second, 128, mem, 1024,
                                                false, true);

      CHECK(curCoord.first == 1);
      CHECK(curCoord.second == Sparse::Coord({0, 0, 0}));
    };
  };

  SECTION("one-page buffer")
  {
    pageTable.Initialise(64, 64);

    CHECK(pageTable.getPageByteSize() == 64);
    CHECK(pageTable.getMipTail().byteOffset == 0);
    CHECK(pageTable.getMipTail().byteStride == 0);
    CHECK(pageTable.getMipTail().totalPackedByteSize == 64);
    CHECK(pageTable.getMipTail().firstMip == 0);
    REQUIRE(pageTable.getMipTail().mappings.size() == 1);

    ResourceId mem = ResourceIDGen::GetNewUniqueID();

    uint64_t nextTailOffset;

    nextTailOffset = pageTable.setBufferRange(0, mem, 1024, 64, false);

    CHECK(nextTailOffset == 64);

    CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
    CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mem, 1024}));

    nextTailOffset = pageTable.setBufferRange(0, ResourceId(), 1024, 64, false);

    CHECK(nextTailOffset == 64);

    CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
    CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({ResourceId(), 0}));
  };

  SECTION("non-page-aligned buffer")
  {
    pageTable.Initialise(100, 64);

    CHECK(pageTable.getPageByteSize() == 64);
    CHECK(pageTable.getMipTail().byteOffset == 0);
    CHECK(pageTable.getMipTail().byteStride == 0);
    CHECK(pageTable.getMipTail().totalPackedByteSize == 100);
    CHECK(pageTable.getMipTail().firstMip == 0);
    REQUIRE(pageTable.getMipTail().mappings.size() == 1);

    uint64_t nextTailOffset;

    ResourceId mem = ResourceIDGen::GetNewUniqueID();

    nextTailOffset = pageTable.setBufferRange(0, mem, 1024, 100, false);

    CHECK(nextTailOffset == 100);

    CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
    CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mem, 1024}));

    nextTailOffset = pageTable.setBufferRange(0, ResourceId(), 1024, 64, false);

    CHECK(nextTailOffset == 64);

    CHECK_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
    REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 2);
    CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem, 1024 + 64}));
  };

  SECTION("sub-page sized buffer")
  {
    pageTable.Initialise(10, 64);

    CHECK(pageTable.getPageByteSize() == 64);
    CHECK(pageTable.getMipTail().byteOffset == 0);
    CHECK(pageTable.getMipTail().byteStride == 0);
    CHECK(pageTable.getMipTail().totalPackedByteSize == 10);
    CHECK(pageTable.getMipTail().firstMip == 0);
    REQUIRE(pageTable.getMipTail().mappings.size() == 1);

    uint64_t nextTailOffset;

    ResourceId mem = ResourceIDGen::GetNewUniqueID();

    nextTailOffset = pageTable.setBufferRange(0, mem, 1024, 10, false);

    CHECK(nextTailOffset == 10);

    CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
    CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mem, 1024}));

    nextTailOffset = pageTable.setBufferRange(0, ResourceId(), 1024, 10, false);

    CHECK(nextTailOffset == 10);

    CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
    CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({ResourceId(), 0}));
  };

  SECTION("64-bit sized buffer")
  {
    const uint32_t k64 = 64 * 1024;
    pageTable.Initialise(10ULL << 30ULL, k64);

    CHECK(pageTable.getPageByteSize() == k64);
    CHECK(pageTable.getMipTail().byteOffset == 0);
    CHECK(pageTable.getMipTail().byteStride == 0);
    CHECK(pageTable.getMipTail().totalPackedByteSize == 10ULL << 30ULL);
    CHECK(pageTable.getMipTail().firstMip == 0);
    REQUIRE(pageTable.getMipTail().mappings.size() == 1);

    uint64_t nextTailOffset;

    ResourceId mem = ResourceIDGen::GetNewUniqueID();

    nextTailOffset = pageTable.setBufferRange(0x200000000ULL, mem, 128 * 1024, 128 * 1024, false);

    CHECK(nextTailOffset == 0x200020000ULL);

    CHECK(!pageTable.getMipTail().mappings[0].hasSingleMapping());
    CHECK(pageTable.getMipTail().mappings[0].pages.size() == (10ULL << 30ULL) / k64);

    size_t idx = size_t(0x200000000ULL / k64);

    CHECK(pageTable.getMipTail().mappings[0].pages[idx - 1].memory == ResourceId());
    CHECK(pageTable.getMipTail().mappings[0].pages[idx - 1].offset == 0);
    CHECK(pageTable.getMipTail().mappings[0].pages[idx + 0].memory == mem);
    CHECK(pageTable.getMipTail().mappings[0].pages[idx + 0].offset == 128 * 1024);
    CHECK(pageTable.getMipTail().mappings[0].pages[idx + 1].memory == mem);
    CHECK(pageTable.getMipTail().mappings[0].pages[idx + 1].offset == 192 * 1024);
    CHECK(pageTable.getMipTail().mappings[0].pages[idx + 2].memory == ResourceId());
    CHECK(pageTable.getMipTail().mappings[0].pages[idx + 2].offset == 0);
  }

  SECTION("2D texture")
  {
    // create a 256x256 texture with 32x32 pages, 6 mips (the last two are in the mip tail)
    pageTable.Initialise({256, 256, 1}, 6, 1, 64, {32, 32, 1}, 4, 0x10000, 0, 256);

    CHECK(pageTable.getPageByteSize() == 64);
    CHECK(pageTable.getPageTexelSize() == Sparse::Coord({32, 32, 1}));
    CHECK(pageTable.getMipTail().byteOffset == 0x10000);
    CHECK(pageTable.getMipTail().byteStride == 0);
    CHECK(pageTable.getMipTail().totalPackedByteSize == 256);
    CHECK(pageTable.getMipTail().firstMip == 4);
    // only expect one mapping because we specified stride of 0, so packed mip tail
    REQUIRE(pageTable.getMipTail().mappings.size() == 1);

    REQUIRE(pageTable.getNumSubresources() == 6);

    CHECK_FALSE(pageTable.isSubresourceInMipTail(0));
    CHECK_FALSE(pageTable.isSubresourceInMipTail(1));
    CHECK_FALSE(pageTable.isSubresourceInMipTail(2));
    CHECK_FALSE(pageTable.isSubresourceInMipTail(3));
    CHECK(pageTable.isSubresourceInMipTail(4));
    CHECK(pageTable.isSubresourceInMipTail(5));

    CHECK_FALSE(pageTable.isByteOffsetInResource(0));
    CHECK_FALSE(pageTable.isByteOffsetInResource(0x1000));
    CHECK(pageTable.isByteOffsetInResource(0x10000));
    CHECK(pageTable.isByteOffsetInResource(0x10000 + 32));
    CHECK(pageTable.isByteOffsetInResource(0x10000 + 255));
    CHECK_FALSE(pageTable.isByteOffsetInResource(0x10000 + 256));

    // they should all be a single mapping to NULL
    CHECK(pageTable.getSubresource(0).hasSingleMapping());
    CHECK(pageTable.getSubresource(0).singleMapping == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(1).hasSingleMapping());
    CHECK(pageTable.getSubresource(1).singleMapping == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(2).hasSingleMapping());
    CHECK(pageTable.getSubresource(2).singleMapping == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(3).hasSingleMapping());
    CHECK(pageTable.getSubresource(3).singleMapping == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(4).hasSingleMapping());
    CHECK(pageTable.getSubresource(4).singleMapping == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(5).hasSingleMapping());
    CHECK(pageTable.getSubresource(5).singleMapping == Sparse::Page({ResourceId(), 0}));

    ResourceId mip = ResourceIDGen::GetNewUniqueID();

    uint64_t nextTailOffset;

    // this is tested above more robustly as buffers. Here we just check that setting the mip tail
    // offset doesn't break anything
    nextTailOffset = pageTable.setMipTailRange(0x10000, mip, 128, 256, false);

    CHECK(nextTailOffset == 0x10000 + 256);

    CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
    CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip, 128}));

    SECTION("whole-subresource bindings")
    {
      ResourceId sub0 = ResourceIDGen::GetNewUniqueID();
      ResourceId sub1 = ResourceIDGen::GetNewUniqueID();
      ResourceId sub2 = ResourceIDGen::GetNewUniqueID();
      ResourceId sub3 = ResourceIDGen::GetNewUniqueID();

      pageTable.setImageBoxRange(0, {0, 0, 0}, {256, 256, 1}, sub0, 0, false);

      CHECK(pageTable.getSubresource(0).hasSingleMapping());
      CHECK(pageTable.getSubresource(0).singleMapping == Sparse::Page({sub0, 0}));
      CHECK_FALSE(pageTable.getSubresource(0).singlePageReused);

      pageTable.setImageBoxRange(1, {0, 0, 0}, {128, 128, 1}, sub1, 128, true);

      CHECK(pageTable.getSubresource(1).hasSingleMapping());
      CHECK(pageTable.getSubresource(1).singleMapping == Sparse::Page({sub1, 128}));
      CHECK(pageTable.getSubresource(1).singlePageReused);

      rdcpair<uint32_t, Sparse::Coord> nextCoord;

      // this mip 2 is 64x64 which is 2x2 tiles, each tiles is 64 bytes
      nextCoord = pageTable.setImageWrappedRange(2, {0, 0, 0}, 2 * 2 * 64, sub2, 256, false);

      CHECK(pageTable.getSubresource(2).hasSingleMapping());
      CHECK(pageTable.getSubresource(2).singleMapping == Sparse::Page({sub2, 256}));
      CHECK_FALSE(pageTable.getSubresource(2).singlePageReused);

      CHECK(nextCoord.first == 3);
      CHECK(nextCoord.second == Sparse::Coord({0, 0, 0}));

      nextCoord = pageTable.setImageWrappedRange(3, {0, 0, 0}, 1 * 1 * 64, sub3, 512, true);

      CHECK(pageTable.getSubresource(3).hasSingleMapping());
      CHECK(pageTable.getSubresource(3).singleMapping == Sparse::Page({sub3, 512}));
      // this is redundant because there's only one page, but let's check it anyway
      CHECK(pageTable.getSubresource(3).singlePageReused);

      CHECK(nextCoord.first == 4);
      CHECK(nextCoord.second == Sparse::Coord({0, 0, 0}));
    };

    SECTION("Wrapped bindings into mip tail")
    {
      ResourceId whole = ResourceIDGen::GetNewUniqueID();
      ResourceId sub = ResourceIDGen::GetNewUniqueID();

      rdcpair<uint32_t, Sparse::Coord> nextCoord;

      // set the entire resource to the same page, that's enough tiles for each subresource plus the
      // mip tail
      nextCoord = pageTable.setImageWrappedRange(
          0, {0, 0, 0}, (8 * 8 + 4 * 4 + 2 * 2 + 1 * 1) * 64 + 256, whole, 512, true);

      CHECK(nextCoord.first == 6);
      CHECK(nextCoord.second == Sparse::Coord({0, 0, 0}));

      for(uint32_t i = 0; i < 4; i++)
      {
        CHECK(pageTable.getSubresource(i).hasSingleMapping());
        CHECK(pageTable.getSubresource(i).singleMapping == Sparse::Page({whole, 512}));
        CHECK(pageTable.getSubresource(i).singlePageReused);
      }

      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({whole, 512}));
      CHECK(pageTable.getMipTail().mappings[0].singlePageReused);

      // set it without being the same page, to check offsets
      nextCoord = pageTable.setImageWrappedRange(
          0, {0, 0, 0}, (8 * 8 + 4 * 4 + 2 * 2 + 1 * 1) * 64 + 256, whole, 0, false);

      CHECK(nextCoord.first == 6);
      CHECK(nextCoord.second == Sparse::Coord({0, 0, 0}));

      uint32_t dim = 8, offset = 0;
      for(uint32_t i = 0; i < 4; i++)
      {
        CHECK(pageTable.getSubresource(i).hasSingleMapping());
        CHECK(pageTable.getSubresource(i).singleMapping == Sparse::Page({whole, offset}));
        CHECK_FALSE(pageTable.getSubresource(i).singlePageReused);

        offset += (dim * dim) * 64;
        dim >>= 1;
      }

      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({whole, offset}));
      CHECK_FALSE(pageTable.getMipTail().mappings[0].singlePageReused);

      // wrap a binding part-way into a mip tail
      nextCoord = pageTable.setImageWrappedRange(3, {0, 0, 0}, 64 + 128, sub, 0, false);

      // the 'coord' for mip tail is effectively 1D. For compatibility with the D3D12 API it returns
      // coords with 1 page worth of texels per tile (since D3D12 we unconditionally multiply the
      // tile region coords with tile size to get texel coords, which also applies to the miptail
      CHECK(nextCoord.first == 4);
      CHECK(nextCoord.second == Sparse::Coord({64, 0, 0}));

      CHECK(pageTable.getSubresource(3).hasSingleMapping());
      CHECK(pageTable.getSubresource(3).singleMapping == Sparse::Page({sub, 0}));
      CHECK_FALSE(pageTable.getSubresource(3).singlePageReused);

      CHECK_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 4);

      // first two pages have been updated
      CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({sub, 64}));
      CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({sub, 128}));

      // second two pages still point into whole
      CHECK(pageTable.getMipTail().mappings[0].pages[2] ==
            Sparse::Page({whole, (8 * 8 + 4 * 4 + 2 * 2 + 1 * 1) * 64 + 128}));
      CHECK(pageTable.getMipTail().mappings[0].pages[3] ==
            Sparse::Page({whole, (8 * 8 + 4 * 4 + 2 * 2 + 1 * 1) * 64 + 192}));
    };

    SECTION("Partial-subresource bindings")
    {
      ResourceId sub0a = ResourceIDGen::GetNewUniqueID();
      ResourceId sub0b = ResourceIDGen::GetNewUniqueID();
      ResourceId sub0c = ResourceIDGen::GetNewUniqueID();

      // make sure that we detect this as a sub-update even though it starts at 0 and has full width
      pageTable.setImageBoxRange(0, {0, 0, 0}, {256, 192, 1}, sub0a, 0, false);

      CHECK_FALSE(pageTable.getSubresource(0).hasSingleMapping());
      // 8x8 pages in top mip
      REQUIRE(pageTable.getSubresource(0).pages.size() == 64);

#define _idx(x, y) y * 8 + x

      // don't check every one, spot-check
      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({sub0a, 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({sub0a, 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({sub0a, 128}));

      CHECK(pageTable.getSubresource(0).pages[_idx(1, 2)] == Sparse::Page({sub0a, (2 * 8 + 1) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 2)] == Sparse::Page({sub0a, (2 * 8 + 2) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 2)] == Sparse::Page({sub0a, (2 * 8 + 3) * 64}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 6)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 6)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(4, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(7, 7)] == Sparse::Page({ResourceId(), 0}));

      // update only a sub-box
      pageTable.setImageBoxRange(0, {64, 0, 0}, {32, 256, 1}, sub0b, 0, false);

      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({sub0a, 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({sub0a, 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({sub0b, 0}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 2)] == Sparse::Page({sub0b, 128}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 2)] == Sparse::Page({sub0a, (2 * 8 + 3) * 64}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 6)] == Sparse::Page({sub0b, 6 * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 6)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 7)] == Sparse::Page({sub0b, 7 * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(4, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(7, 7)] == Sparse::Page({ResourceId(), 0}));

      rdcpair<uint32_t, Sparse::Coord> nextCoord;

      // update a wrapped region
      nextCoord = pageTable.setImageWrappedRange(0, {96, 192, 0}, 8 * 64, sub0c, 640, true);

      CHECK(nextCoord.first == 0);
      CHECK(nextCoord.second == Sparse::Coord({3, 7, 0}));

      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({sub0a, 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({sub0a, 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({sub0b, 0}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 2)] == Sparse::Page({sub0b, 128}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 2)] == Sparse::Page({sub0a, (2 * 8 + 3) * 64}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 6)] == Sparse::Page({sub0b, 6 * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 6)] == Sparse::Page({sub0c, 640}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 7)] == Sparse::Page({sub0c, 640}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 7)] == Sparse::Page({sub0c, 640}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(4, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(7, 7)] == Sparse::Page({ResourceId(), 0}));

      nextCoord = pageTable.setImageWrappedRange(0, {64, 224, 0}, 11 * 64, sub0c, 6400, false);

      CHECK(nextCoord.first == 1);
      CHECK(nextCoord.second == Sparse::Coord({1, 1, 0}));

      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({sub0a, 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({sub0a, 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({sub0b, 0}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 2)] == Sparse::Page({sub0b, 128}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 2)] == Sparse::Page({sub0a, (2 * 8 + 3) * 64}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 6)] == Sparse::Page({sub0b, 6 * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 6)] == Sparse::Page({sub0c, 640}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 7)] == Sparse::Page({sub0c, 640}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 7)] == Sparse::Page({sub0c, 6400}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 7)] == Sparse::Page({sub0c, 6464}));
      CHECK(pageTable.getSubresource(0).pages[_idx(4, 7)] == Sparse::Page({sub0c, 6528}));
      CHECK(pageTable.getSubresource(0).pages[_idx(7, 7)] == Sparse::Page({sub0c, 6720}));

      CHECK_FALSE(pageTable.getSubresource(1).hasSingleMapping());
      // 4x4 pages in second mip
      REQUIRE(pageTable.getSubresource(1).pages.size() == 16);
      CHECK(pageTable.getSubresource(1).pages[0] == Sparse::Page({sub0c, 6784}));

      nextCoord = pageTable.setImageWrappedRange(0, {32, 0, 0}, 64, ResourceId(), 640, false);

      CHECK(nextCoord.first == 0);
      CHECK(nextCoord.second == Sparse::Coord({2, 0, 0}));

      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({sub0a, 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({sub0b, 0}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 2)] == Sparse::Page({sub0b, 128}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 2)] == Sparse::Page({sub0a, (2 * 8 + 3) * 64}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 6)] == Sparse::Page({sub0b, 6 * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 6)] == Sparse::Page({sub0c, 640}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 7)] == Sparse::Page({sub0c, 640}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 7)] == Sparse::Page({sub0c, 6400}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 7)] == Sparse::Page({sub0c, 6464}));
      CHECK(pageTable.getSubresource(0).pages[_idx(4, 7)] == Sparse::Page({sub0c, 6528}));
      CHECK(pageTable.getSubresource(0).pages[_idx(7, 7)] == Sparse::Page({sub0c, 6720}));

      pageTable.setImageBoxRange(0, {32, 192, 0}, {64, 64, 1}, ResourceId(), 640, false);

      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({sub0a, 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({sub0b, 0}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 2)] == Sparse::Page({sub0b, 128}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 2)] == Sparse::Page({sub0a, (2 * 8 + 3) * 64}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 6)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 6)] == Sparse::Page({sub0c, 640}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 7)] == Sparse::Page({sub0c, 6464}));
      CHECK(pageTable.getSubresource(0).pages[_idx(4, 7)] == Sparse::Page({sub0c, 6528}));
      CHECK(pageTable.getSubresource(0).pages[_idx(7, 7)] == Sparse::Page({sub0c, 6720}));

      nextCoord = pageTable.setImageWrappedRange(0, {128, 224, 0}, 64 * 4, sub0a, 512, true);

      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({sub0a, 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({sub0b, 0}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 2)] == Sparse::Page({sub0b, 128}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 2)] == Sparse::Page({sub0a, (2 * 8 + 3) * 64}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 6)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 6)] == Sparse::Page({sub0c, 640}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 7)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 7)] == Sparse::Page({sub0c, 6464}));
      CHECK(pageTable.getSubresource(0).pages[_idx(4, 7)] == Sparse::Page({sub0a, 512}));
      CHECK(pageTable.getSubresource(0).pages[_idx(7, 7)] == Sparse::Page({sub0a, 512}));

      CHECK(pageTable.getSubresource(1).pages[0] == Sparse::Page({sub0c, 6784}));

      CHECK(nextCoord.first == 1);
      CHECK(nextCoord.second == Sparse::Coord({0, 0, 0}));
    };
  };

  SECTION("2D rectangular texture")
  {
    pageTable.Initialise({512, 128, 1}, 6, 1, 64, {32, 32, 1}, 4, 0x10000, 0, 64);

    ResourceId mem0 = ResourceIDGen::GetNewUniqueID();
    ResourceId mem1 = ResourceIDGen::GetNewUniqueID();
    ResourceId mem2 = ResourceIDGen::GetNewUniqueID();

    pageTable.setImageBoxRange(0, {0, 0, 0}, {256, 64, 1}, mem0, 0, true);

    CHECK_FALSE(pageTable.getSubresource(0).hasSingleMapping());
    // 16x4 pages in top mip
    REQUIRE(pageTable.getSubresource(0).pages.size() == 64);

#undef _idx
#define _idx(x, y) y * 16 + x

    CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({mem0, 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(1, 1)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(2, 1)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(3, 1)] == Sparse::Page({mem0, 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(11, 2)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(12, 2)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(13, 2)] == Sparse::Page({ResourceId(), 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(11, 3)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(12, 3)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(13, 3)] == Sparse::Page({ResourceId(), 0}));

    pageTable.setImageBoxRange(0, {256, 64, 0}, {256, 64, 1}, mem1, 0, true);

    CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({mem0, 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(1, 1)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(2, 1)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(3, 1)] == Sparse::Page({mem0, 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(11, 2)] == Sparse::Page({mem1, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(12, 2)] == Sparse::Page({mem1, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(13, 2)] == Sparse::Page({mem1, 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(11, 3)] == Sparse::Page({mem1, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(12, 3)] == Sparse::Page({mem1, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(13, 3)] == Sparse::Page({mem1, 0}));

    rdcpair<uint32_t, Sparse::Coord> nextCoord;

    // update from 11,2 for 17 tiles, which should overlap correctly to 11,3 and no more
    nextCoord = pageTable.setImageWrappedRange(0, {11 * 32, 64, 0}, 64 * 17, mem2, 0, true);

    CHECK(nextCoord.first == 0);
    CHECK(nextCoord.second == Sparse::Coord({12, 3, 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({mem0, 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(1, 1)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(2, 1)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(3, 1)] == Sparse::Page({mem0, 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(11, 2)] == Sparse::Page({mem2, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(12, 2)] == Sparse::Page({mem2, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(13, 2)] == Sparse::Page({mem2, 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(11, 3)] == Sparse::Page({mem2, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(12, 3)] == Sparse::Page({mem1, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(13, 3)] == Sparse::Page({mem1, 0}));
  };

  SECTION("2D non-aligned texture")
  {
    pageTable.Initialise({500, 116, 1}, 6, 1, 64, {32, 32, 1}, 4, 0x10000, 0, 64);

    ResourceId mem0 = ResourceIDGen::GetNewUniqueID();
    ResourceId mem1 = ResourceIDGen::GetNewUniqueID();
    ResourceId mem2 = ResourceIDGen::GetNewUniqueID();
    ResourceId mem3 = ResourceIDGen::GetNewUniqueID();

#undef _idx
#define _idx(x, y) y * 16 + x

    pageTable.setImageBoxRange(0, {0, 0, 0}, {256, 64, 1}, mem0, 0, true);
    pageTable.setImageBoxRange(0, {256, 64, 0}, {500 - 256, 116 - 64, 1}, mem1, 0, true);
    pageTable.setImageWrappedRange(0, {11 * 32, 64, 0}, 64 * 17, mem2, 0, true);

    // still 16x4 pages in top mip
    REQUIRE(pageTable.getSubresource(0).pages.size() == 64);

    CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({mem0, 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(1, 1)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(2, 1)] == Sparse::Page({mem0, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(3, 1)] == Sparse::Page({mem0, 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(11, 2)] == Sparse::Page({mem2, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(12, 2)] == Sparse::Page({mem2, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(13, 2)] == Sparse::Page({mem2, 0}));

    CHECK(pageTable.getSubresource(0).pages[_idx(11, 3)] == Sparse::Page({mem2, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(12, 3)] == Sparse::Page({mem1, 0}));
    CHECK(pageTable.getSubresource(0).pages[_idx(13, 3)] == Sparse::Page({mem1, 0}));

    pageTable.setImageBoxRange(1, {224, 0, 0}, {26, 32, 1}, mem3, 0, true);

    CHECK(pageTable.getSubresource(1).pages[_idx(7, 0)] == Sparse::Page({mem3, 0}));
  };

  SECTION("2D texture that's all mip tail")
  {
    // create a 256x256 texture with 32x32 pages, 6 mips (the last two are in the mip tail)
    pageTable.Initialise({256, 256, 1}, 6, 1, 64, {32, 32, 1}, 0, 0, 0, 8192);

    CHECK(pageTable.getPageByteSize() == 64);
    CHECK(pageTable.getPageTexelSize() == Sparse::Coord({32, 32, 1}));
    CHECK(pageTable.getMipTail().byteOffset == 0);
    CHECK(pageTable.getMipTail().byteStride == 0);
    CHECK(pageTable.getMipTail().totalPackedByteSize == 8192);
    CHECK(pageTable.getMipTail().firstMip == 0);
    REQUIRE(pageTable.getMipTail().mappings.size() == 1);

    REQUIRE(pageTable.getNumSubresources() == 6);

    CHECK(pageTable.isSubresourceInMipTail(0));
    CHECK(pageTable.isSubresourceInMipTail(1));
    CHECK(pageTable.isSubresourceInMipTail(2));
    CHECK(pageTable.isSubresourceInMipTail(3));
    CHECK(pageTable.isSubresourceInMipTail(4));
    CHECK(pageTable.isSubresourceInMipTail(5));

    ResourceId mip = ResourceIDGen::GetNewUniqueID();

    uint64_t nextTailOffset;

    // this is tested above more robustly as buffers. Here we just check that setting the mip tail
    // offset doesn't break anything
    nextTailOffset = pageTable.setMipTailRange(0, mip, 512, 256, false);

    CHECK(nextTailOffset == 256);

    CHECK_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
    REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 8192 / 64);
    CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mip, 512}));
    CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mip, 576}));
    CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mip, 640}));
    CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({mip, 704}));
    CHECK(pageTable.getMipTail().mappings[0].pages[4] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getMipTail().mappings[0].pages[5] == Sparse::Page({ResourceId(), 0}));
  };

  SECTION("mip tails smaller than the page size")
  {
    ResourceId mip = ResourceIDGen::GetNewUniqueID();

    SECTION("normal texture")
    {
      pageTable.Initialise({256, 256, 1}, 6, 1, 65536, {32, 32, 1}, 4, 0, 0, 8192);

      uint64_t nextTailOffset;

      nextTailOffset = pageTable.setMipTailRange(0, mip, 65536 * 3, 8192, false);

      CHECK(nextTailOffset == 8192);
      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 0);
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip, 65536 * 3}));

      nextTailOffset = pageTable.setMipTailRange(0, mip, 65536 * 3, 65536, false);

      CHECK(nextTailOffset == 8192);
      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 0);
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip, 65536 * 3}));

      rdcpair<uint32_t, Sparse::Coord> nextCoord;
      // setImageWrappedRange doesn't support anything but page-sized byte sizes to set
      nextCoord = pageTable.setImageWrappedRange(4, {0, 0, 0}, 65536, mip, 65536 * 10, false, true);

      CHECK(nextCoord.first == 6);
      CHECK(nextCoord.second == Sparse::Coord({0, 0, 0}));
      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 0);
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip, 65536 * 10}));
    };

    SECTION("texture that's all mip tail")
    {
      pageTable.Initialise({256, 256, 1}, 6, 1, 65536, {32, 32, 1}, 0, 0, 0, 8192);

      uint64_t nextTailOffset;

      nextTailOffset = pageTable.setMipTailRange(0, mip, 65536 * 3, 8192, false);

      CHECK(nextTailOffset == 8192);
      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 0);
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip, 65536 * 3}));

      nextTailOffset = pageTable.setMipTailRange(0, mip, 65536 * 3, 65536, false);

      CHECK(nextTailOffset == 8192);
      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 0);
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip, 65536 * 3}));

      rdcpair<uint32_t, Sparse::Coord> nextCoord;
      // setImageWrappedRange doesn't support anything but page-sized byte sizes to set
      nextCoord = pageTable.setImageWrappedRange(0, {0, 0, 0}, 65536, mip, 65536 * 10, false, true);

      CHECK(nextCoord.first == 6);
      CHECK(nextCoord.second == Sparse::Coord({0, 0, 0}));
      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 0);
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip, 65536 * 10}));
    };

    SECTION("small no-mips texture")
    {
      pageTable.Initialise({8, 8, 1}, 1, 1, 65536, {32, 32, 1}, 0, 0, 0, 8192);

      uint64_t nextTailOffset;

      nextTailOffset = pageTable.setMipTailRange(0, mip, 65536 * 3, 8192, false);

      CHECK(nextTailOffset == 8192);
      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 0);
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip, 65536 * 3}));

      nextTailOffset = pageTable.setMipTailRange(0, mip, 65536 * 3, 65536, false);

      CHECK(nextTailOffset == 8192);
      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 0);
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip, 65536 * 3}));

      rdcpair<uint32_t, Sparse::Coord> nextCoord;
      // setImageWrappedRange doesn't support anything but page-sized byte sizes to set
      nextCoord = pageTable.setImageWrappedRange(0, {0, 0, 0}, 65536, mip, 65536 * 10, false, true);

      CHECK(nextCoord.first == 1);
      CHECK(nextCoord.second == Sparse::Coord({0, 0, 0}));
      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 0);
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip, 65536 * 10}));
    };
  };

  SECTION("2D texture being set with unbounded range")
  {
    ResourceId whole = ResourceIDGen::GetNewUniqueID();

    // create a 256x256 texture with 32x32 pages, 6 mips (the last two are in the mip tail)
    pageTable.Initialise({256, 256, 1}, 6, 1, 64, {32, 32, 1}, 4, 0, 0, 8192);

    pageTable.setImageWrappedRange(0, {0, 0, 0}, ~0U, whole, 0, false);

    CHECK(pageTable.getSubresource(0).hasSingleMapping());
    CHECK(pageTable.getSubresource(0).singleMapping == Sparse::Page({whole, 0}));
    CHECK(pageTable.getSubresource(1).hasSingleMapping());
    CHECK(pageTable.getSubresource(1).singleMapping == Sparse::Page({whole, (8 * 8) * 64}));
    CHECK(pageTable.getSubresource(2).hasSingleMapping());
    CHECK(pageTable.getSubresource(2).singleMapping == Sparse::Page({whole, (8 * 8 + 4 * 4) * 64}));
    CHECK(pageTable.getSubresource(3).hasSingleMapping());
    CHECK(pageTable.getSubresource(3).singleMapping ==
          Sparse::Page({whole, (8 * 8 + 4 * 4 + 2 * 2) * 64}));

    CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
    CHECK(pageTable.getMipTail().mappings[0].singleMapping ==
          Sparse::Page({whole, (8 * 8 + 4 * 4 + 2 * 2 + 1 * 1) * 64}));
  };

  SECTION("3D texture tests")
  {
    // create a 256x256x64 texture with 32x32x4 pages, 6 mips (the last two are in the mip tail)
    pageTable.Initialise({256, 256, 64}, 6, 1, 64, {32, 32, 4}, 4, 0x10000, 0, 64);

    CHECK(pageTable.getPageByteSize() == 64);
    CHECK(pageTable.getPageTexelSize() == Sparse::Coord({32, 32, 4}));
    CHECK(pageTable.getMipTail().byteOffset == 0x10000);
    CHECK(pageTable.getMipTail().byteStride == 0);
    CHECK(pageTable.getMipTail().totalPackedByteSize == 64);
    CHECK(pageTable.getMipTail().firstMip == 4);

    REQUIRE(pageTable.getNumSubresources() == 6);

    CHECK_FALSE(pageTable.isSubresourceInMipTail(0));
    CHECK_FALSE(pageTable.isSubresourceInMipTail(1));
    CHECK_FALSE(pageTable.isSubresourceInMipTail(2));
    CHECK_FALSE(pageTable.isSubresourceInMipTail(3));
    CHECK(pageTable.isSubresourceInMipTail(4));
    CHECK(pageTable.isSubresourceInMipTail(5));

    // they should all be a single mapping to NULL
    CHECK(pageTable.getSubresource(0).hasSingleMapping());
    CHECK(pageTable.getSubresource(0).singleMapping == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(1).hasSingleMapping());
    CHECK(pageTable.getSubresource(1).singleMapping == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(2).hasSingleMapping());
    CHECK(pageTable.getSubresource(2).singleMapping == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(3).hasSingleMapping());
    CHECK(pageTable.getSubresource(3).singleMapping == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(4).hasSingleMapping());
    CHECK(pageTable.getSubresource(4).singleMapping == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(5).hasSingleMapping());
    CHECK(pageTable.getSubresource(5).singleMapping == Sparse::Page({ResourceId(), 0}));

    SECTION("whole-subresource bindings")
    {
      ResourceId sub0 = ResourceIDGen::GetNewUniqueID();
      ResourceId sub1 = ResourceIDGen::GetNewUniqueID();

      pageTable.setImageBoxRange(0, {0, 0, 0}, {256, 256, 64}, sub0, 0, false);

      CHECK(pageTable.getSubresource(0).hasSingleMapping());
      CHECK(pageTable.getSubresource(0).singleMapping == Sparse::Page({sub0, 0}));
      CHECK_FALSE(pageTable.getSubresource(0).singlePageReused);

      pageTable.setImageBoxRange(1, {0, 0, 0}, {128, 128, 32}, sub1, 128, true);

      CHECK(pageTable.getSubresource(1).hasSingleMapping());
      CHECK(pageTable.getSubresource(1).singleMapping == Sparse::Page({sub1, 128}));
      CHECK(pageTable.getSubresource(1).singlePageReused);
    };

    SECTION("Partial-subresource bindings")
    {
      ResourceId sub0a = ResourceIDGen::GetNewUniqueID();
      ResourceId sub0b = ResourceIDGen::GetNewUniqueID();
      ResourceId sub0c = ResourceIDGen::GetNewUniqueID();

      // make sure that we detect this as a sub-update even though it covers full width/height
      pageTable.setImageBoxRange(0, {0, 0, 0}, {256, 256, 16}, sub0a, 0, false);

      CHECK_FALSE(pageTable.getSubresource(0).hasSingleMapping());
      // 8x8x16 pages in top mip
      REQUIRE(pageTable.getSubresource(0).pages.size() == 8 * 8 * 16);

#undef _idx
#define _idx(x, y, z) ((z * 8 + y) * 8 + x)

      // don't check every one, spot-check
      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0, 0)] ==
            Sparse::Page({sub0a, _idx(0, 0, 0) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 4, 0)] ==
            Sparse::Page({sub0a, _idx(3, 4, 0) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(7, 7, 0)] ==
            Sparse::Page({sub0a, _idx(7, 7, 0) * 64}));

      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0, 1)] ==
            Sparse::Page({sub0a, _idx(0, 0, 1) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 4, 1)] ==
            Sparse::Page({sub0a, _idx(3, 4, 1) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(7, 7, 1)] ==
            Sparse::Page({sub0a, _idx(7, 7, 1) * 64}));

      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0, 10)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 4, 10)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(7, 7, 10)] == Sparse::Page({ResourceId(), 0}));

      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0, 11)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 4, 11)] == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(0).pages[_idx(7, 7, 11)] == Sparse::Page({ResourceId(), 0}));
    };
  };

  SECTION("2D texture array tests")
  {
    ResourceId mip0 = ResourceIDGen::GetNewUniqueID();
    ResourceId mip1 = ResourceIDGen::GetNewUniqueID();
    ResourceId mip2 = ResourceIDGen::GetNewUniqueID();

    // create a 256x256 texture with 32x32 pages, 6 mips (the last two are in the mip tail), and 5
    // array slices. The mip tail in this case we make two pages to better show the effect of the
    // mip tail
    SECTION("separate mip tail")
    {
      // in the event that we have separate mip tails the stride may be huge as otherwise it's just
      // a single mip tail storage. In this event we don't want to overallocate and waste pages
      pageTable.Initialise({256, 256, 1}, 6, 5, 64, {32, 32, 1}, 4, 0x10000, 32768, 128 * 5);

      SECTION("property accessors")
      {
        CHECK(pageTable.getMipCount() == 6);
        CHECK(pageTable.getArraySize() == 5);

        CHECK(pageTable.calcSubresource(0, 0) == 0);
        CHECK(pageTable.calcSubresource(0, 1) == 1);
        CHECK(pageTable.calcSubresource(0, 2) == 2);
        CHECK(pageTable.calcSubresource(0, 3) == 3);
        CHECK(pageTable.calcSubresource(0, 4) == 4);
        CHECK(pageTable.calcSubresource(0, 5) == 5);
        CHECK(pageTable.calcSubresource(1, 0) == 6);
        CHECK(pageTable.calcSubresource(2, 2) == 14);
        CHECK(pageTable.calcSubresource(4, 5) == 29);

        // 64 bytes per page, 8x8 pages in top mip
        CHECK(pageTable.getSubresourceByteSize(0) == 64 * 8 * 8);
        CHECK(pageTable.getSubresourceByteSize(6) == 64 * 8 * 8);
        CHECK(pageTable.getSubresourceByteSize(12) == 64 * 8 * 8);
        CHECK(pageTable.getSubresourceByteSize(1) == 64 * 4 * 4);
        CHECK(pageTable.getSubresourceByteSize(2) == 64 * 2 * 2);
        CHECK(pageTable.getSubresourceByteSize(3) == 64 * 1 * 1);
      }

      CHECK(pageTable.getPageByteSize() == 64);
      CHECK(pageTable.getPageTexelSize() == Sparse::Coord({32, 32, 1}));
      CHECK(pageTable.getMipTail().byteOffset == 0x10000);
      CHECK(pageTable.getMipTail().byteStride == 32768);
      CHECK(pageTable.getMipTail().totalPackedByteSize == 128 * 5);
      CHECK(pageTable.getMipTail().firstMip == 4);
      REQUIRE(pageTable.getMipTail().mappings.size() == 5);

      REQUIRE(pageTable.getNumSubresources() == 6 * 5);

      CHECK_FALSE(pageTable.isByteOffsetInResource(0));
      CHECK_FALSE(pageTable.isByteOffsetInResource(0x1000));
      CHECK(pageTable.isByteOffsetInResource(0x10000));
      CHECK(pageTable.isByteOffsetInResource(0x10000 + 32));
      CHECK(pageTable.isByteOffsetInResource(0x10000 + 1280));
      CHECK(pageTable.isByteOffsetInResource(0x10000 + 32768));
      CHECK(pageTable.isByteOffsetInResource(0x10000 + 128000));
      CHECK(pageTable.isByteOffsetInResource(0x10000 + 32768 * 5 - 1));
      CHECK_FALSE(pageTable.isByteOffsetInResource(0x10000 + 32768 * 5));

      // all mips in the same array slice should have the same miptail offset
      CHECK(pageTable.getMipTailByteOffsetForSubresource(0) ==
            pageTable.getMipTailByteOffsetForSubresource(1));
      CHECK(pageTable.getMipTailByteOffsetForSubresource(6) ==
            pageTable.getMipTailByteOffsetForSubresource(8));
      CHECK(pageTable.getMipTailByteOffsetForSubresource(18) ==
            pageTable.getMipTailByteOffsetForSubresource(20));

      // but mips in different slices should have a different one
      CHECK(pageTable.getMipTailByteOffsetForSubresource(0) !=
            pageTable.getMipTailByteOffsetForSubresource(6));
      CHECK(pageTable.getMipTailByteOffsetForSubresource(0) !=
            pageTable.getMipTailByteOffsetForSubresource(20));

      // the calculated offset should be relative to the stride, not relative to the packing
      CHECK(pageTable.getMipTailByteOffsetForSubresource(6) == 0x10000 + 32768);

      uint64_t nextTailOffset;

      SECTION("separate whole-mip sets")
      {
        nextTailOffset = pageTable.setMipTailRange(pageTable.getMipTailByteOffsetForSubresource(0),
                                                   mip0, 0, 128, false);

        CHECK(nextTailOffset == pageTable.getMipTailByteOffsetForSubresource(6));

        nextTailOffset = pageTable.setMipTailRange(pageTable.getMipTailByteOffsetForSubresource(6),
                                                   mip1, 640, 128, true);

        CHECK(nextTailOffset == pageTable.getMipTailByteOffsetForSubresource(12));

        nextTailOffset = pageTable.setMipTailRange(pageTable.getMipTailByteOffsetForSubresource(18),
                                                   mip2, 6400, 128, false);

        CHECK(nextTailOffset == pageTable.getMipTailByteOffsetForSubresource(24));

        // each of these sets should have been detected as a single page mapping
        CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip0, 0}));
        CHECK_FALSE(pageTable.getMipTail().mappings[0].singlePageReused);
        CHECK(pageTable.getMipTail().mappings[1].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[1].singleMapping == Sparse::Page({mip1, 640}));
        CHECK(pageTable.getMipTail().mappings[1].singlePageReused);
        CHECK(pageTable.getMipTail().mappings[2].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[2].singleMapping == Sparse::Page({ResourceId(), 0}));
        CHECK_FALSE(pageTable.getMipTail().mappings[2].singlePageReused);
        CHECK(pageTable.getMipTail().mappings[3].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[3].singleMapping == Sparse::Page({mip2, 6400}));
        CHECK_FALSE(pageTable.getMipTail().mappings[3].singlePageReused);
        CHECK(pageTable.getMipTail().mappings[4].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[4].singleMapping == Sparse::Page({ResourceId(), 0}));
        CHECK_FALSE(pageTable.getMipTail().mappings[4].singlePageReused);
      };

      SECTION("single set large enough for all mips")
      {
        nextTailOffset = pageTable.setMipTailRange(pageTable.getMipTailByteOffsetForSubresource(0),
                                                   mip0, 0, 32768 * 4 + 128, false);

        CHECK(nextTailOffset >= pageTable.getMipTailByteOffsetForSubresource(29) + 128);

        CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip0, 32768 * 0}));
        CHECK_FALSE(pageTable.getMipTail().mappings[0].singlePageReused);
        CHECK(pageTable.getMipTail().mappings[1].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[1].singleMapping == Sparse::Page({mip0, 32768 * 1}));
        CHECK_FALSE(pageTable.getMipTail().mappings[1].singlePageReused);
        CHECK(pageTable.getMipTail().mappings[2].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[2].singleMapping == Sparse::Page({mip0, 32768 * 2}));
        CHECK_FALSE(pageTable.getMipTail().mappings[2].singlePageReused);
        CHECK(pageTable.getMipTail().mappings[3].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[3].singleMapping == Sparse::Page({mip0, 32768 * 3}));
        CHECK_FALSE(pageTable.getMipTail().mappings[3].singlePageReused);
        CHECK(pageTable.getMipTail().mappings[4].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[4].singleMapping == Sparse::Page({mip0, 32768 * 4}));
        CHECK_FALSE(pageTable.getMipTail().mappings[4].singlePageReused);
      };

      SECTION("Partial and overlapping memory sets")
      {
        nextTailOffset = pageTable.setMipTailRange(pageTable.getMipTailByteOffsetForSubresource(0),
                                                   mip0, 0, 64, false);

        CHECK(nextTailOffset == pageTable.getMipTailByteOffsetForSubresource(0) + 64);

        nextTailOffset = pageTable.setMipTailRange(
            pageTable.getMipTailByteOffsetForSubresource(6) + 64, mip1, 256, 64, false);

        CHECK(nextTailOffset == pageTable.getMipTailByteOffsetForSubresource(12));

        CHECK_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
        REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 2);
        CHECK_FALSE(pageTable.getMipTail().mappings[0].singlePageReused);
        CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mip0, 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({ResourceId(), 0}));

        CHECK_FALSE(pageTable.getMipTail().mappings[1].hasSingleMapping());
        REQUIRE(pageTable.getMipTail().mappings[1].pages.size() == 2);
        CHECK(pageTable.getMipTail().mappings[1].pages[0] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[1].pages[1] == Sparse::Page({mip1, 256}));

        // this set is dubiously legal in client APIs but we ensure it works. We set part of one mip
        // tail, then the whole stride (which overwrites the real non-tail subresources?) then part
        // of the mip tail of the next
        // we set 64 bytes in one, 'set' (skip) the padding bytes (stride - miptail size) then 64
        // more bytes
        nextTailOffset =
            pageTable.setMipTailRange(pageTable.getMipTailByteOffsetForSubresource(0) + 64, mip2,
                                      64, 64 + (32768 - 128) + 64, false);

        CHECK(nextTailOffset == pageTable.getMipTailByteOffsetForSubresource(6) + 64);

        CHECK_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
        REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 2);
        CHECK_FALSE(pageTable.getMipTail().mappings[0].singlePageReused);
        CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mip0, 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mip2, 64}));

        CHECK_FALSE(pageTable.getMipTail().mappings[1].hasSingleMapping());
        REQUIRE(pageTable.getMipTail().mappings[1].pages.size() == 2);
        CHECK(pageTable.getMipTail().mappings[1].pages[0] == Sparse::Page({mip2, 32768}));
        CHECK(pageTable.getMipTail().mappings[1].pages[1] == Sparse::Page({mip1, 256}));

        nextTailOffset =
            pageTable.setMipTailRange(pageTable.getMipTailByteOffsetForSubresource(18) + 64, mip2,
                                      0, 64 + (32768 - 128) + 128, false);

        CHECK(nextTailOffset >= pageTable.getMipTailByteOffsetForSubresource(29) + 128);

        CHECK_FALSE(pageTable.getMipTail().mappings[3].hasSingleMapping());
        REQUIRE(pageTable.getMipTail().mappings[3].pages.size() == 2);
        CHECK(pageTable.getMipTail().mappings[3].pages[0] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[3].pages[1] == Sparse::Page({mip2, 0}));

        CHECK(pageTable.getMipTail().mappings[4].hasSingleMapping());
        CHECK_FALSE(pageTable.getMipTail().mappings[4].singlePageReused);
        CHECK(pageTable.getMipTail().mappings[4].singleMapping ==
              Sparse::Page({mip2, 64 + (32768 - 128)}));
      };
    };

    SECTION("combined mip tail")
    {
      pageTable.Initialise({256, 256, 1}, 6, 5, 64, {32, 32, 1}, 4, 0x10000, 0, 128 * 5);

      CHECK(pageTable.getPageByteSize() == 64);
      CHECK(pageTable.getPageTexelSize() == Sparse::Coord({32, 32, 1}));
      CHECK(pageTable.getMipTail().byteOffset == 0x10000);
      CHECK(pageTable.getMipTail().byteStride == 0);
      CHECK(pageTable.getMipTail().totalPackedByteSize == 128 * 5);
      CHECK(pageTable.getMipTail().firstMip == 4);
      REQUIRE(pageTable.getMipTail().mappings.size() == 1);

      REQUIRE(pageTable.getNumSubresources() == 6 * 5);

      CHECK_FALSE(pageTable.isByteOffsetInResource(0));
      CHECK_FALSE(pageTable.isByteOffsetInResource(0x1000));
      CHECK(pageTable.isByteOffsetInResource(0x10000));
      CHECK(pageTable.isByteOffsetInResource(0x10000 + 32));
      CHECK(pageTable.isByteOffsetInResource(0x10000 + 128));
      CHECK(pageTable.isByteOffsetInResource(0x10000 + 128 * 5 - 1));
      CHECK_FALSE(pageTable.isByteOffsetInResource(0x10000 + 128 * 5));

      // all mips in all array slices should have the same miptail offset we specified
      CHECK(pageTable.getMipTailByteOffsetForSubresource(0) == 0x10000);
      CHECK(pageTable.getMipTailByteOffsetForSubresource(0) ==
            pageTable.getMipTailByteOffsetForSubresource(1));

      CHECK(pageTable.getMipTailByteOffsetForSubresource(0) ==
            pageTable.getMipTailByteOffsetForSubresource(6));

      CHECK(pageTable.getMipTailByteOffsetForSubresource(0) ==
            pageTable.getMipTailByteOffsetForSubresource(8));

      CHECK(pageTable.getMipTailByteOffsetForSubresource(0) ==
            pageTable.getMipTailByteOffsetForSubresource(16));

      CHECK(pageTable.getMipTailByteOffsetForSubresource(0) ==
            pageTable.getMipTailByteOffsetForSubresource(20));

      uint64_t nextTailOffset;

      SECTION("whole-tail set")
      {
        nextTailOffset = pageTable.setMipTailRange(0x10000, mip0, 0, 128 * 5, false);

        CHECK(nextTailOffset == 0x10000 + 128 * 5);
        CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mip0, 0}));
        CHECK_FALSE(pageTable.getMipTail().mappings[0].singlePageReused);
      };

      SECTION("separate mip sets")
      {
        // we don't use getMipTailByteOffset.. to calculate the offset because the mip tail is a
        // single one for all subresources
        nextTailOffset = pageTable.setMipTailRange(0x10000, mip0, 0, 128, false);

        CHECK(nextTailOffset == 0x10000 + 128);

        nextTailOffset = pageTable.setMipTailRange(0x10000 + 128, mip1, 640, 128, false);

        CHECK(nextTailOffset == 0x10000 + 256);

        nextTailOffset = pageTable.setMipTailRange(0x10000 + 384, mip2, 6400, 128, false);

        CHECK(nextTailOffset == 0x10000 + 512);

        // we should only allocate the minimum number of pages - total size divided by page size
        CHECK_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
        REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 5 * 2);

        CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mip0, 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mip0, 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mip1, 640}));
        CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({mip1, 704}));
        CHECK(pageTable.getMipTail().mappings[0].pages[4] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[5] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[6] == Sparse::Page({mip2, 6400}));
        CHECK(pageTable.getMipTail().mappings[0].pages[7] == Sparse::Page({mip2, 6464}));
      };
    };

    SECTION("no mip tail")
    {
      pageTable.Initialise({256, 256, 1}, 6, 5, 64, {32, 32, 1}, 8, 0x10000, 0, 128 * 5);

      CHECK(pageTable.getPageByteSize() == 64);
      CHECK(pageTable.getPageTexelSize() == Sparse::Coord({32, 32, 1}));
      CHECK(pageTable.getMipTail().byteOffset == 0);
      CHECK(pageTable.getMipTail().byteStride == 0);
      CHECK(pageTable.getMipTail().totalPackedByteSize == 0);
      CHECK(pageTable.getMipTail().firstMip == 6);

      REQUIRE(pageTable.getNumSubresources() == 6 * 5);

      CHECK_FALSE(pageTable.isByteOffsetInResource(0));
      CHECK_FALSE(pageTable.isByteOffsetInResource(0x1000));
      CHECK_FALSE(pageTable.isByteOffsetInResource(0x10000));
      CHECK_FALSE(pageTable.isByteOffsetInResource(0x10000 + 32));
      CHECK_FALSE(pageTable.isByteOffsetInResource(0x10000 + 63));
      CHECK_FALSE(pageTable.isByteOffsetInResource(0x10000 + 64));
    };

    if(pageTable.getMipTail().totalPackedByteSize > 0)
    {
      for(uint32_t slice = 0; slice < 5; slice++)
      {
        for(uint32_t mip = 0; mip < 6; mip++)
        {
          uint32_t sub = slice * 6 + mip;
          if(mip < 4)
          {
            CHECK_FALSE(pageTable.isSubresourceInMipTail(sub));
          }
          else
          {
            CHECK(pageTable.isSubresourceInMipTail(sub));
          }
        }
      }
    }

    ResourceId sub0 = ResourceIDGen::GetNewUniqueID();
    ResourceId sub1_2 = ResourceIDGen::GetNewUniqueID();
    ResourceId sub7 = ResourceIDGen::GetNewUniqueID();
    ResourceId sub8 = ResourceIDGen::GetNewUniqueID();
    ResourceId sub18_19_20 = ResourceIDGen::GetNewUniqueID();

    pageTable.setImageBoxRange(0, {0, 0, 0}, {256, 256, 1}, sub0, 0, false);

    CHECK(pageTable.getSubresource(0).hasSingleMapping());
    CHECK(pageTable.getSubresource(0).singleMapping == Sparse::Page({sub0, 0}));
    CHECK_FALSE(pageTable.getSubresource(0).singlePageReused);

    rdcpair<uint32_t, Sparse::Coord> nextCoord;

    // this will set all of subresource 1 (4x4 tiles), wrap into subresource 2 (2x2 tiles) and set
    // all of that
    nextCoord = pageTable.setImageWrappedRange(1, {0, 0, 0}, (16 + 4) * 64, sub1_2, 0x200000, false);

    CHECK(nextCoord.first == 3);
    CHECK(nextCoord.second == Sparse::Coord({0, 0, 0}));

    CHECK(pageTable.getSubresource(1).hasSingleMapping());
    CHECK(pageTable.getSubresource(1).singleMapping == Sparse::Page({sub1_2, 0x200000}));
    CHECK_FALSE(pageTable.getSubresource(1).singlePageReused);

    CHECK(pageTable.getSubresource(2).hasSingleMapping());
    CHECK(pageTable.getSubresource(2).singleMapping == Sparse::Page({sub1_2, 0x200000 + 16 * 64}));
    CHECK_FALSE(pageTable.getSubresource(2).singlePageReused);

    CHECK(pageTable.getSubresource(3).hasSingleMapping());
    CHECK(pageTable.getSubresource(3).singleMapping == Sparse::Page({ResourceId(), 0}));

    pageTable.setImageBoxRange(7, {0, 0, 0}, {128, 128, 1}, sub7, 128, true);

    CHECK(pageTable.getSubresource(7).hasSingleMapping());
    CHECK(pageTable.getSubresource(7).singleMapping == Sparse::Page({sub7, 128}));
    CHECK(pageTable.getSubresource(7).singlePageReused);

#undef _idx
#define _idx(x, y) y * 2 + x

    pageTable.setImageBoxRange(8, {32, 0, 0}, {32, 64, 1}, sub8, 12800, false);

    CHECK(pageTable.getSubresource(8).pages[_idx(0, 0)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(8).pages[_idx(1, 0)] == Sparse::Page({sub8, 12800}));

    CHECK(pageTable.getSubresource(8).pages[_idx(0, 1)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(8).pages[_idx(1, 1)] == Sparse::Page({sub8, 12864}));

    // this sets some of subresource 18 (8x8 tiles), all of subresource 19 (4x4 tiles) and some of
    // 20 (2x2 tiles)
    nextCoord =
        pageTable.setImageWrappedRange(18, {128, 128, 0}, (28 + 16 + 1) * 64, sub18_19_20, 0, false);

    CHECK(nextCoord.first == 20);
    CHECK(nextCoord.second == Sparse::Coord({1, 0, 0}));

#undef _idx
#define _idx(x, y) y * 8 + x

    CHECK(pageTable.getSubresource(18).pages[_idx(0, 0)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(18).pages[_idx(3, 3)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(18).pages[_idx(4, 3)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(18).pages[_idx(5, 3)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(18).pages[_idx(3, 4)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(18).pages[_idx(4, 4)] == Sparse::Page({sub18_19_20, 0 * 64}));
    CHECK(pageTable.getSubresource(18).pages[_idx(5, 4)] == Sparse::Page({sub18_19_20, 1 * 64}));
    CHECK(pageTable.getSubresource(18).pages[_idx(7, 7)] == Sparse::Page({sub18_19_20, 27 * 64}));

    CHECK(pageTable.getSubresource(19).hasSingleMapping());
    CHECK(pageTable.getSubresource(19).singleMapping == Sparse::Page({sub18_19_20, 28 * 64}));
    CHECK_FALSE(pageTable.getSubresource(19).singlePageReused);

#undef _idx
#define _idx(x, y) y * 2 + x

    CHECK(pageTable.getSubresource(20).pages[_idx(0, 0)] ==
          Sparse::Page({sub18_19_20, (28 + 16) * 64}));
    CHECK(pageTable.getSubresource(20).pages[_idx(1, 0)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(20).pages[_idx(0, 1)] == Sparse::Page({ResourceId(), 0}));
    CHECK(pageTable.getSubresource(20).pages[_idx(1, 1)] == Sparse::Page({ResourceId(), 0}));
  };

  SECTION("Updates from whole-subresource to split pages")
  {
    ResourceId mem0 = ResourceIDGen::GetNewUniqueID();
    ResourceId mem1 = ResourceIDGen::GetNewUniqueID();
    ResourceId mem2 = ResourceIDGen::GetNewUniqueID();

    SECTION("Buffers/mip-tail")
    {
      pageTable.Initialise(320, 64);

      CHECK(pageTable.getPageByteSize() == 64);
      CHECK(pageTable.getMipTail().byteOffset == 0);
      CHECK(pageTable.getMipTail().byteStride == 0);
      CHECK(pageTable.getMipTail().totalPackedByteSize == 320);
      CHECK(pageTable.getMipTail().firstMip == 0);
      REQUIRE(pageTable.getMipTail().mappings.size() == 1);

      uint64_t nextTailOffset;

      nextTailOffset = pageTable.setBufferRange(0, mem0, 0, 320, false);

      CHECK(nextTailOffset == 320);

      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mem0, 0}));
      CHECK_FALSE(pageTable.getMipTail().mappings[0].singlePageReused);

      nextTailOffset = pageTable.setBufferRange(128, mem1, 0, 64, false);

      CHECK(nextTailOffset == 128 + 64);

      CHECK_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 5);
      CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem0, 0}));
      CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem0, 64}));
      CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem1, 0}));
      CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({mem0, 192}));
      CHECK(pageTable.getMipTail().mappings[0].pages[4] == Sparse::Page({mem0, 256}));

      nextTailOffset = pageTable.setBufferRange(0, mem2, 1024, 64, false);

      CHECK(nextTailOffset == 0 + 64);

      CHECK_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
      REQUIRE(pageTable.getMipTail().mappings[0].pages.size() == 5);
      CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem2, 1024}));
      CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem0, 64}));
      CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem1, 0}));
      CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({mem0, 192}));
      CHECK(pageTable.getMipTail().mappings[0].pages[4] == Sparse::Page({mem0, 256}));

      nextTailOffset = pageTable.setBufferRange(0, mem2, 0, 320, false);

      CHECK(nextTailOffset == 0 + 320);

      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mem2, 0}));
      CHECK_FALSE(pageTable.getMipTail().mappings[0].singlePageReused);

      nextTailOffset = pageTable.setBufferRange(0, mem1, 0, 320, true);

      CHECK(nextTailOffset == 0 + 320);

      CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
      CHECK(pageTable.getMipTail().mappings[0].singleMapping == Sparse::Page({mem1, 0}));
      CHECK(pageTable.getMipTail().mappings[0].singlePageReused);
    };

    SECTION("2D texture")
    {
      // create a 256x256 texture with 32x32 pages, 6 mips (the last two are in the mip tail)
      pageTable.Initialise({256, 256, 1}, 6, 1, 64, {32, 32, 1}, 4, 0x10000, 0, 64);

      // they should all be a single mapping to NULL
      CHECK(pageTable.getSubresource(0).hasSingleMapping());
      CHECK(pageTable.getSubresource(0).singleMapping == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(1).hasSingleMapping());
      CHECK(pageTable.getSubresource(1).singleMapping == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(2).hasSingleMapping());
      CHECK(pageTable.getSubresource(2).singleMapping == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(3).hasSingleMapping());
      CHECK(pageTable.getSubresource(3).singleMapping == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(4).hasSingleMapping());
      CHECK(pageTable.getSubresource(4).singleMapping == Sparse::Page({ResourceId(), 0}));
      CHECK(pageTable.getSubresource(5).hasSingleMapping());
      CHECK(pageTable.getSubresource(5).singleMapping == Sparse::Page({ResourceId(), 0}));

      pageTable.setImageBoxRange(0, {0, 0, 0}, {256, 256, 1}, mem0, 0, false);

      CHECK(pageTable.getSubresource(0).hasSingleMapping());
      CHECK(pageTable.getSubresource(0).singleMapping == Sparse::Page({mem0, 0}));
      CHECK_FALSE(pageTable.getSubresource(0).singlePageReused);

      pageTable.setImageBoxRange(0, {32, 32, 0}, {64, 64, 1}, mem1, 10240, true);

#undef _idx
#define _idx(x, y) (y * 8 + x)

      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({mem0, _idx(0, 0) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({mem0, _idx(1, 0) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({mem0, _idx(2, 0) * 64}));

      CHECK(pageTable.getSubresource(0).pages[_idx(1, 1)] == Sparse::Page({mem1, 10240}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 1)] == Sparse::Page({mem1, 10240}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 2)] == Sparse::Page({mem1, 10240}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 2)] == Sparse::Page({mem1, 10240}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 6)] == Sparse::Page({mem0, _idx(2, 6) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 6)] == Sparse::Page({mem0, _idx(3, 6) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 7)] == Sparse::Page({mem0, _idx(1, 7) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 7)] == Sparse::Page({mem0, _idx(2, 7) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 7)] == Sparse::Page({mem0, _idx(3, 7) * 64}));

      pageTable.setImageBoxRange(0, {0, 0, 0}, {256, 256, 1}, mem0, 0, false);
      pageTable.setImageBoxRange(0, {32, 32, 0}, {64, 64, 1}, mem1, 1024000, false);

      CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({mem0, _idx(0, 0) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({mem0, _idx(1, 0) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({mem0, _idx(2, 0) * 64}));

      CHECK(pageTable.getSubresource(0).pages[_idx(1, 1)] == Sparse::Page({mem1, 1024000}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 1)] == Sparse::Page({mem1, 1024064}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 2)] == Sparse::Page({mem1, 1024128}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 2)] == Sparse::Page({mem1, 1024192}));

      CHECK(pageTable.getSubresource(0).pages[_idx(2, 6)] == Sparse::Page({mem0, _idx(2, 6) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 6)] == Sparse::Page({mem0, _idx(3, 6) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(1, 7)] == Sparse::Page({mem0, _idx(1, 7) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(2, 7)] == Sparse::Page({mem0, _idx(2, 7) * 64}));
      CHECK(pageTable.getSubresource(0).pages[_idx(3, 7)] == Sparse::Page({mem0, _idx(3, 7) * 64}));
    };
  };

  SECTION("page table copy operations")
  {
    ResourceId mem0 = ResourceIDGen::GetNewUniqueID();
    ResourceId mem1 = ResourceIDGen::GetNewUniqueID();
    ResourceId mem2 = ResourceIDGen::GetNewUniqueID();
    ResourceId mem3 = ResourceIDGen::GetNewUniqueID();
    ResourceId mem4 = ResourceIDGen::GetNewUniqueID();

    Sparse::PageTable srcPageTable;

    SECTION("Buffers")
    {
      SECTION("Same size")
      {
        pageTable.Initialise(320, 64);
        srcPageTable.Initialise(320, 64);

        srcPageTable.setBufferRange(0, mem0, 0, 320, false);

        pageTable.copyImageBoxRange(0, {0, 0, 0}, {5, 1, 1}, srcPageTable, 0, {0, 0, 0});

        CHECK(srcPageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].singleMapping ==
              srcPageTable.getMipTail().mappings[0].singleMapping);

        // copying two separate boxes won't coalesce
        pageTable.copyImageBoxRange(0, {0, 0, 0}, {4, 1, 1}, srcPageTable, 0, {0, 0, 0});
        pageTable.copyImageBoxRange(0, {4, 0, 0}, {1, 1, 1}, srcPageTable, 0, {4, 0, 0});

        CHECK(srcPageTable.getMipTail().mappings[0].hasSingleMapping());
        REQUIRE_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem0, 0 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem0, 1 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem0, 2 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({mem0, 3 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[4] == Sparse::Page({mem0, 4 * 64}));

        pageTable.copyImageWrappedRange(0, {0, 0, 0}, 5, srcPageTable, 0, {0, 0, 0});

        CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].singleMapping ==
              srcPageTable.getMipTail().mappings[0].singleMapping);

        pageTable.copyImageWrappedRange(0, {0, 0, 0}, 4, srcPageTable, 0, {0, 0, 0});
        pageTable.copyImageWrappedRange(0, {4, 0, 0}, 1, srcPageTable, 0, {4, 0, 0});

        REQUIRE_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem0, 0 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem0, 1 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem0, 2 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({mem0, 3 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[4] == Sparse::Page({mem0, 4 * 64}));
      };

      SECTION("Source larger")
      {
        pageTable.Initialise(320, 64);
        srcPageTable.Initialise(640, 64);

        srcPageTable.setBufferRange(0, mem0, 0, 640, false);

        pageTable.copyImageBoxRange(0, {0, 0, 0}, {5, 1, 1}, srcPageTable, 0, {0, 0, 0});

        CHECK(srcPageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].singleMapping ==
              srcPageTable.getMipTail().mappings[0].singleMapping);

        // copying two separate boxes won't coalesce
        pageTable.copyImageBoxRange(0, {0, 0, 0}, {4, 1, 1}, srcPageTable, 0, {0, 0, 0});
        pageTable.copyImageBoxRange(0, {4, 0, 0}, {1, 1, 1}, srcPageTable, 0, {4, 0, 0});

        CHECK(srcPageTable.getMipTail().mappings[0].hasSingleMapping());
        REQUIRE_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem0, 0 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem0, 1 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem0, 2 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({mem0, 3 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[4] == Sparse::Page({mem0, 4 * 64}));

        pageTable.copyImageWrappedRange(0, {0, 0, 0}, 5, srcPageTable, 0, {0, 0, 0});

        CHECK(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].singleMapping ==
              srcPageTable.getMipTail().mappings[0].singleMapping);

        pageTable.copyImageWrappedRange(0, {0, 0, 0}, 4, srcPageTable, 0, {0, 0, 0});
        pageTable.copyImageWrappedRange(0, {4, 0, 0}, 1, srcPageTable, 0, {4, 0, 0});

        REQUIRE_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem0, 0 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem0, 1 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem0, 2 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({mem0, 3 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[4] == Sparse::Page({mem0, 4 * 64}));
      };

      SECTION("Destination larger")
      {
        pageTable.Initialise(640, 64);
        srcPageTable.Initialise(320, 64);

        srcPageTable.setBufferRange(0, mem0, 0, 320, false);

        pageTable.copyImageBoxRange(0, {0, 0, 0}, {5, 1, 1}, srcPageTable, 0, {0, 0, 0});

        CHECK(srcPageTable.getMipTail().mappings[0].hasSingleMapping());
        REQUIRE_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem0, 0 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem0, 1 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem0, 2 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({mem0, 3 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[4] == Sparse::Page({mem0, 4 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[5] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[6] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[7] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[8] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[9] == Sparse::Page({ResourceId(), 0}));

        // copying two separate boxes won't coalesce
        pageTable.copyImageBoxRange(0, {0, 0, 0}, {4, 1, 1}, srcPageTable, 0, {0, 0, 0});
        pageTable.copyImageBoxRange(0, {4, 0, 0}, {1, 1, 1}, srcPageTable, 0, {4, 0, 0});

        REQUIRE_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem0, 0 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem0, 1 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem0, 2 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({mem0, 3 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[4] == Sparse::Page({mem0, 4 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[5] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[6] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[7] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[8] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[9] == Sparse::Page({ResourceId(), 0}));

        pageTable.copyImageWrappedRange(0, {0, 0, 0}, 5, srcPageTable, 0, {0, 0, 0});

        REQUIRE_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem0, 0 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem0, 1 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem0, 2 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({mem0, 3 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[4] == Sparse::Page({mem0, 4 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[5] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[6] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[7] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[8] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[9] == Sparse::Page({ResourceId(), 0}));

        pageTable.copyImageWrappedRange(0, {0, 0, 0}, 4, srcPageTable, 0, {0, 0, 0});
        pageTable.copyImageWrappedRange(0, {4, 0, 0}, 1, srcPageTable, 0, {4, 0, 0});

        REQUIRE_FALSE(pageTable.getMipTail().mappings[0].hasSingleMapping());
        CHECK(pageTable.getMipTail().mappings[0].pages[0] == Sparse::Page({mem0, 0 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[1] == Sparse::Page({mem0, 1 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[2] == Sparse::Page({mem0, 2 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[3] == Sparse::Page({mem0, 3 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[4] == Sparse::Page({mem0, 4 * 64}));
        CHECK(pageTable.getMipTail().mappings[0].pages[5] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[6] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[7] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[8] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getMipTail().mappings[0].pages[9] == Sparse::Page({ResourceId(), 0}));
      };
    };

    SECTION("2D textures")
    {
      pageTable.Initialise({256, 256, 1}, 6, 1, 64, {32, 32, 1}, 4, 0x10000, 0x10000, 256);
      srcPageTable.Initialise({256, 256, 1}, 6, 1, 64, {32, 32, 1}, 4, 0x10000, 0x10000, 256);

      SECTION("Box copies")
      {
        srcPageTable.setImageBoxRange(0, {0, 0, 0}, {256, 256, 1}, mem0, 0, false);
        srcPageTable.setImageBoxRange(1, {0, 0, 0}, {128, 128, 1}, mem1, 0, false);
        srcPageTable.setImageBoxRange(2, {0, 0, 0}, {64, 64, 1}, mem2, 0, false);

        pageTable.copyImageBoxRange(0, {0, 0, 0}, {8, 8, 1}, srcPageTable, 0, {0, 0, 0});

        CHECK(pageTable.getSubresource(0).hasSingleMapping());
        CHECK(pageTable.getSubresource(0).singleMapping == Sparse::Page({mem0, 0}));

        pageTable.copyImageBoxRange(1, {0, 0, 0}, {4, 4, 1}, srcPageTable, 1, {0, 0, 0});

        CHECK(pageTable.getSubresource(1).hasSingleMapping());
        CHECK(pageTable.getSubresource(1).singleMapping == Sparse::Page({mem1, 0}));

        srcPageTable.setImageBoxRange(0, {0, 0, 0}, {256, 256, 1}, mem3, 0, false);

        pageTable.copyImageBoxRange(0, {0, 0, 0}, {4, 4, 1}, srcPageTable, 0, {0, 0, 0});

        REQUIRE_FALSE(pageTable.getSubresource(0).hasSingleMapping());

#undef _idx
#define _idx(x, y) (y * 8 + x)

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({mem3, _idx(0, 0) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({mem3, _idx(1, 0) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({mem3, _idx(2, 0) * 64}));

        CHECK(pageTable.getSubresource(0).pages[_idx(5, 0)] == Sparse::Page({mem0, _idx(5, 0) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(6, 0)] == Sparse::Page({mem0, _idx(6, 0) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(7, 0)] == Sparse::Page({mem0, _idx(7, 0) * 64}));

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 1)] == Sparse::Page({mem3, _idx(0, 1) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 1)] == Sparse::Page({mem3, _idx(1, 1) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 1)] == Sparse::Page({mem3, _idx(2, 1) * 64}));

        pageTable.copyImageBoxRange(0, {0, 0, 0}, {4, 4, 1}, srcPageTable, 1, {0, 0, 0});

        REQUIRE_FALSE(pageTable.getSubresource(0).hasSingleMapping());
        CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({mem1, 0 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({mem1, 1 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({mem1, 2 * 64}));

        CHECK(pageTable.getSubresource(0).pages[_idx(5, 0)] == Sparse::Page({mem0, _idx(5, 0) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(6, 0)] == Sparse::Page({mem0, _idx(6, 0) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(7, 0)] == Sparse::Page({mem0, _idx(7, 0) * 64}));

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 1)] == Sparse::Page({mem1, 4 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 1)] == Sparse::Page({mem1, 5 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 1)] == Sparse::Page({mem1, 6 * 64}));

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 3)] == Sparse::Page({mem1, 12 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 3)] == Sparse::Page({mem1, 13 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 3)] == Sparse::Page({mem1, 14 * 64}));

        CHECK(pageTable.getSubresource(0).pages[_idx(4, 4)] == Sparse::Page({mem0, _idx(4, 4) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(5, 4)] == Sparse::Page({mem0, _idx(5, 4) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(6, 4)] == Sparse::Page({mem0, _idx(6, 4) * 64}));

        CHECK(pageTable.getSubresource(0).pages[_idx(4, 5)] == Sparse::Page({mem0, _idx(4, 5) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(5, 5)] == Sparse::Page({mem0, _idx(5, 5) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(6, 5)] == Sparse::Page({mem0, _idx(6, 5) * 64}));

        pageTable.copyImageBoxRange(0, {4, 4, 0}, {4, 4, 1}, srcPageTable, 1, {0, 0, 0});

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({mem1, 0 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({mem1, 1 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({mem1, 2 * 64}));

        CHECK(pageTable.getSubresource(0).pages[_idx(5, 0)] == Sparse::Page({mem0, _idx(5, 0) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(6, 0)] == Sparse::Page({mem0, _idx(6, 0) * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(7, 0)] == Sparse::Page({mem0, _idx(7, 0) * 64}));

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 1)] == Sparse::Page({mem1, 4 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 1)] == Sparse::Page({mem1, 5 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 1)] == Sparse::Page({mem1, 6 * 64}));

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 3)] == Sparse::Page({mem1, 12 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 3)] == Sparse::Page({mem1, 13 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 3)] == Sparse::Page({mem1, 14 * 64}));

        CHECK(pageTable.getSubresource(0).pages[_idx(4, 4)] == Sparse::Page({mem1, 0 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(5, 4)] == Sparse::Page({mem1, 1 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(6, 4)] == Sparse::Page({mem1, 2 * 64}));

        CHECK(pageTable.getSubresource(0).pages[_idx(4, 5)] == Sparse::Page({mem1, 4 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(5, 5)] == Sparse::Page({mem1, 5 * 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(6, 5)] == Sparse::Page({mem1, 6 * 64}));
      };

      SECTION("Wrapped copies preserving single mapping")
      {
        srcPageTable.setImageBoxRange(0, {0, 0, 0}, {256, 256, 1}, mem0, 0, false);
        srcPageTable.setImageBoxRange(1, {0, 0, 0}, {128, 128, 1}, mem1, 0, false);
        srcPageTable.setImageBoxRange(2, {0, 0, 0}, {64, 64, 1}, mem2, 0, false);

        pageTable.copyImageWrappedRange(0, {0, 0, 0}, 8 * 8, srcPageTable, 0, {0, 0, 0});

        CHECK(pageTable.getSubresource(0).hasSingleMapping());
        CHECK(pageTable.getSubresource(0).singleMapping == Sparse::Page({mem0, 0}));

        pageTable.copyImageWrappedRange(1, {0, 0, 0}, 4 * 4, srcPageTable, 1, {0, 0, 0});

        CHECK(pageTable.getSubresource(1).hasSingleMapping());
        CHECK(pageTable.getSubresource(1).singleMapping == Sparse::Page({mem1, 0}));

        // copying from a larger mip into a smaller one just copies the tiles literally, even if
        // the tiles don't wrap the same way in each case
        pageTable.copyImageWrappedRange(1, {0, 0, 0}, 4 * 4, srcPageTable, 0, {0, 0, 0});

        CHECK(pageTable.getSubresource(1).hasSingleMapping());
        CHECK(pageTable.getSubresource(1).singleMapping == Sparse::Page({mem0, 0}));

        // this copies the top 3 subresources all together
        pageTable.copyImageWrappedRange(0, {0, 0, 0}, (8 * 8 + 4 * 4 + 2 * 2), srcPageTable, 0,
                                        {0, 0, 0});

        CHECK(pageTable.getSubresource(0).hasSingleMapping());
        CHECK(pageTable.getSubresource(0).singleMapping == Sparse::Page({mem0, 0}));
        CHECK(pageTable.getSubresource(1).hasSingleMapping());
        CHECK(pageTable.getSubresource(1).singleMapping == Sparse::Page({mem1, 0}));
        CHECK(pageTable.getSubresource(2).hasSingleMapping());
        CHECK(pageTable.getSubresource(2).singleMapping == Sparse::Page({mem2, 0}));
      };

      SECTION("Wrapped copies splitting pages")
      {
        srcPageTable.setImageBoxRange(0, {0, 0, 0}, {256, 256, 1}, mem0, 0, false);
        srcPageTable.setImageBoxRange(1, {0, 0, 0}, {128, 128, 1}, mem1, 0, false);
        srcPageTable.setImageBoxRange(2, {0, 0, 0}, {64, 64, 1}, mem2, 0, false);

        pageTable.copyImageWrappedRange(0, {5, 0, 0}, 2, srcPageTable, 0, {0, 0, 0});

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({ResourceId(), 0}));

        CHECK(pageTable.getSubresource(0).pages[_idx(5, 0)] == Sparse::Page({mem0, 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(6, 0)] == Sparse::Page({mem0, 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(7, 0)] == Sparse::Page({ResourceId(), 0}));

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 1)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 1)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 1)] == Sparse::Page({ResourceId(), 0}));

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 3)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 3)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 3)] == Sparse::Page({ResourceId(), 0}));

        CHECK(pageTable.getSubresource(0).pages[_idx(5, 3)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(6, 3)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(7, 3)] == Sparse::Page({ResourceId(), 0}));

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 4)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 4)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 4)] == Sparse::Page({ResourceId(), 0}));

        pageTable.copyImageWrappedRange(0, {0, 3, 0}, 10, srcPageTable, 1, {0, 0, 0});

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 0)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 0)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 0)] == Sparse::Page({ResourceId(), 0}));

        CHECK(pageTable.getSubresource(0).pages[_idx(5, 0)] == Sparse::Page({mem0, 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(6, 0)] == Sparse::Page({mem0, 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(7, 0)] == Sparse::Page({ResourceId(), 0}));

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 1)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 1)] == Sparse::Page({ResourceId(), 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 1)] == Sparse::Page({ResourceId(), 0}));

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 3)] == Sparse::Page({mem1, 0}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 3)] == Sparse::Page({mem1, 64}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 3)] == Sparse::Page({mem1, 128}));

        CHECK(pageTable.getSubresource(0).pages[_idx(5, 3)] == Sparse::Page({mem1, 320}));
        CHECK(pageTable.getSubresource(0).pages[_idx(6, 3)] == Sparse::Page({mem1, 384}));
        CHECK(pageTable.getSubresource(0).pages[_idx(7, 3)] == Sparse::Page({mem1, 448}));

        CHECK(pageTable.getSubresource(0).pages[_idx(0, 4)] == Sparse::Page({mem1, 512}));
        CHECK(pageTable.getSubresource(0).pages[_idx(1, 4)] == Sparse::Page({mem1, 576}));
        CHECK(pageTable.getSubresource(0).pages[_idx(2, 4)] == Sparse::Page({ResourceId(), 0}));
      };
    };
  };

  SECTION("Test unmapped checks")
  {
    // create a 256x256 texture with 32x32 pages, 6 mips (the last two are in the mip tail)
    pageTable.Initialise({256, 256, 1}, 6, 1, 64, {32, 32, 1}, 4, 0, 0, 8192);

    CHECK_FALSE(pageTable.getSubresource(0).isMapped());
    CHECK_FALSE(pageTable.getSubresource(1).isMapped());
    CHECK_FALSE(pageTable.getSubresource(2).isMapped());
    CHECK_FALSE(pageTable.getSubresource(3).isMapped());
    CHECK_FALSE(pageTable.getMipTail().mappings[0].isMapped());
  };

  SECTION("Test unmapped simplification")
  {
    // create a 256x256 texture with 32x32 pages, 6 mips (the last two are in the mip tail)
    pageTable.Initialise({256, 256, 1}, 6, 1, 64, {32, 32, 1}, 4, 0, 0, 8192);

    CHECK_FALSE(pageTable.getSubresource(0).isMapped());
    CHECK_FALSE(pageTable.getSubresource(1).isMapped());
    CHECK_FALSE(pageTable.getSubresource(2).isMapped());
    CHECK_FALSE(pageTable.getSubresource(3).isMapped());
    CHECK_FALSE(pageTable.getMipTail().mappings[0].isMapped());

    ResourceId mem = ResourceIDGen::GetNewUniqueID();

    pageTable.setMipTailRange(0, mem, 128, 256, false);

    CHECK_FALSE(pageTable.getSubresource(0).isMapped());
    CHECK_FALSE(pageTable.getSubresource(1).isMapped());
    CHECK_FALSE(pageTable.getSubresource(2).isMapped());
    CHECK_FALSE(pageTable.getSubresource(3).isMapped());
    CHECK(pageTable.getMipTail().mappings[0].isMapped());

    pageTable.setMipTailRange(0, ResourceId(), 128, 256, false);

    CHECK_FALSE(pageTable.getSubresource(0).isMapped());
    CHECK_FALSE(pageTable.getSubresource(1).isMapped());
    CHECK_FALSE(pageTable.getSubresource(2).isMapped());
    CHECK_FALSE(pageTable.getSubresource(3).isMapped());
    CHECK_FALSE(pageTable.getMipTail().mappings[0].isMapped());

    pageTable.setImageWrappedRange(0, {0, 0, 0}, ~0U, mem, 0, false);

    CHECK(pageTable.getSubresource(0).isMapped());
    CHECK(pageTable.getSubresource(1).isMapped());
    CHECK(pageTable.getSubresource(2).isMapped());
    CHECK(pageTable.getSubresource(3).isMapped());
    CHECK(pageTable.getMipTail().mappings[0].isMapped());

    ResourceId mem2 = ResourceIDGen::GetNewUniqueID();

    pageTable.setImageBoxRange(0, {0, 0, 0}, {32, 32, 1}, mem2, 1024, false);

    CHECK(pageTable.getSubresource(0).isMapped());

    pageTable.setImageBoxRange(0, {64, 0, 0}, {32, 32, 1}, ResourceId(), 1024, false);

    CHECK(pageTable.getSubresource(0).isMapped());

    pageTable.setImageBoxRange(0, {0, 0, 0}, {32, 32, 1}, ResourceId(), 1024, false);

    CHECK(pageTable.getSubresource(0).isMapped());

    pageTable.setImageBoxRange(0, {0, 0, 0}, {256, 192, 1}, ResourceId(), 1024, false);

    CHECK(pageTable.getSubresource(0).isMapped());

    pageTable.setImageBoxRange(0, {0, 192, 0}, {256, 64, 1}, ResourceId(), 1024, false);

    CHECK_FALSE(pageTable.getSubresource(0).isMapped());
  };
};

#endif    // ENABLED(ENABLE_UNIT_TESTS)
