rasterizer_cache: Support multi-level surfaces
* With this commit the cache can now directly upload and use mipmaps without needing to sync them with watchers. By using native mimaps directly this also adds support for mipmap for cube * Since watchers have been removed texture cubes still work but are uncached so slower as well. Will be fixed soon.
This commit is contained in:
@ -64,6 +64,8 @@ bool CheckFormatsBlittable(PixelFormat source_format, PixelFormat dest_format) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_WARNING(HW_GPU, "Unblittable format pair detected {} and {}",
|
||||
PixelFormatAsString(source_format), PixelFormatAsString(dest_format));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -122,14 +122,13 @@ bool RasterizerCache<T>::AccelerateTextureCopy(const GPU::Regs::DisplayTransferC
|
||||
ASSERT(src_rect.GetWidth() == dst_rect.GetWidth());
|
||||
|
||||
const TextureCopy texture_copy = {
|
||||
.src_level = 0,
|
||||
.dst_level = 0,
|
||||
.src_level = src_surface->LevelOf(src_params.addr),
|
||||
.dst_level = dst_surface->LevelOf(dst_params.addr),
|
||||
.src_offset = {src_rect.left, src_rect.bottom},
|
||||
.dst_offset = {dst_rect.left, dst_rect.bottom},
|
||||
.extent = {src_rect.GetWidth(), src_rect.GetHeight()},
|
||||
};
|
||||
runtime.CopyTextures(*src_surface, *dst_surface, texture_copy);
|
||||
dst_surface->InvalidateAllWatcher();
|
||||
|
||||
InvalidateRegion(dst_params.addr, dst_params.size, dst_surface);
|
||||
return true;
|
||||
@ -179,13 +178,12 @@ bool RasterizerCache<T>::AccelerateDisplayTransfer(const GPU::Regs::DisplayTrans
|
||||
}
|
||||
|
||||
const TextureBlit texture_blit = {
|
||||
.src_level = 0,
|
||||
.dst_level = 0,
|
||||
.src_level = src_surface->LevelOf(src_params.addr),
|
||||
.dst_level = dst_surface->LevelOf(dst_params.addr),
|
||||
.src_rect = src_rect,
|
||||
.dst_rect = dst_rect,
|
||||
};
|
||||
runtime.BlitTextures(*src_surface, *dst_surface, texture_blit);
|
||||
dst_surface->InvalidateAllWatcher();
|
||||
|
||||
InvalidateRegion(dst_params.addr, dst_params.size, dst_surface);
|
||||
return true;
|
||||
@ -374,14 +372,14 @@ void RasterizerCache<T>::CopySurface(const Surface& src_surface, const Surface&
|
||||
SurfaceInterval copy_interval) {
|
||||
MICROPROFILE_SCOPE(RasterizerCache_CopySurface);
|
||||
|
||||
const PAddr copy_addr = copy_interval.lower();
|
||||
const auto subrect_params = dst_surface->FromInterval(copy_interval);
|
||||
const Rect2D dst_rect = dst_surface->GetScaledSubRect(subrect_params);
|
||||
const PAddr copy_addr = copy_interval.lower();
|
||||
ASSERT(subrect_params.GetInterval() == copy_interval && src_surface != dst_surface);
|
||||
|
||||
if (src_surface->type == SurfaceType::Fill) {
|
||||
const TextureClear texture_clear = {
|
||||
.texture_level = 0,
|
||||
.texture_level = dst_surface->LevelOf(copy_addr),
|
||||
.texture_rect = dst_rect,
|
||||
.value = src_surface->MakeClearValue(copy_addr, dst_surface->pixel_format),
|
||||
};
|
||||
@ -390,10 +388,8 @@ void RasterizerCache<T>::CopySurface(const Surface& src_surface, const Surface&
|
||||
}
|
||||
|
||||
const TextureBlit texture_blit = {
|
||||
.src_level = 0,
|
||||
.dst_level = 0,
|
||||
.src_layer = 0,
|
||||
.dst_layer = 0,
|
||||
.src_level = src_surface->LevelOf(copy_addr),
|
||||
.dst_level = dst_surface->LevelOf(copy_addr),
|
||||
.src_rect = src_surface->GetScaledSubRect(subrect_params),
|
||||
.dst_rect = dst_rect,
|
||||
};
|
||||
@ -494,6 +490,7 @@ auto RasterizerCache<T>::GetSurfaceSubRect(const SurfaceParams& params, ScaleMat
|
||||
new_params.size = new_params.end - new_params.addr;
|
||||
new_params.height =
|
||||
new_params.size / aligned_params.BytesInPixels(aligned_params.stride);
|
||||
new_params.UpdateParams();
|
||||
ASSERT(new_params.size % aligned_params.BytesInPixels(aligned_params.stride) == 0);
|
||||
|
||||
Surface new_surface = CreateSurface(new_params);
|
||||
@ -501,7 +498,6 @@ auto RasterizerCache<T>::GetSurfaceSubRect(const SurfaceParams& params, ScaleMat
|
||||
|
||||
// Delete the expanded surface, this can't be done safely yet
|
||||
// because it may still be in use
|
||||
surface->UnlinkAllWatcher(); // unlink watchers as if this surface is already deleted
|
||||
remove_surfaces.push_back(surface);
|
||||
|
||||
surface = new_surface;
|
||||
@ -561,64 +557,7 @@ auto RasterizerCache<T>::GetTextureSurface(const Pica::Texture::TextureInfo& inf
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto surface = GetSurface(params, ScaleMatch::Ignore, true);
|
||||
if (!surface) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Update mipmap if necessary
|
||||
if (max_level != 0) {
|
||||
if (max_level >= 8) {
|
||||
// Since PICA only supports texture size between 8 and 1024, there are at most eight
|
||||
// possible mipmap levels including the base.
|
||||
LOG_CRITICAL(HW_GPU, "Unsupported mipmap level {}", max_level);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Blit mipmaps that have been invalidated
|
||||
SurfaceParams surface_params = *surface;
|
||||
for (u32 level = 1; level <= max_level; level++) {
|
||||
// In PICA all mipmap levels are stored next to each other
|
||||
surface_params.addr +=
|
||||
surface_params.width * surface_params.height * surface_params.GetFormatBpp() / 8;
|
||||
surface_params.width /= 2;
|
||||
surface_params.height /= 2;
|
||||
surface_params.stride = 0; // reset stride and let UpdateParams re-initialize it
|
||||
surface_params.levels = 1;
|
||||
surface_params.UpdateParams();
|
||||
|
||||
auto& watcher = surface->level_watchers[level - 1];
|
||||
if (!watcher || !watcher->Get()) {
|
||||
auto level_surface = GetSurface(surface_params, ScaleMatch::Ignore, true);
|
||||
if (level_surface) {
|
||||
watcher = level_surface->CreateWatcher();
|
||||
} else {
|
||||
watcher = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (watcher && !watcher->IsValid()) {
|
||||
auto level_surface =
|
||||
std::static_pointer_cast<typename T::SurfaceType>(watcher->Get());
|
||||
if (!level_surface->invalid_regions.empty()) {
|
||||
ValidateSurface(level_surface, level_surface->addr, level_surface->size);
|
||||
}
|
||||
|
||||
const TextureBlit texture_blit = {
|
||||
.src_level = 0,
|
||||
.dst_level = level,
|
||||
.src_layer = 0,
|
||||
.dst_layer = 0,
|
||||
.src_rect = level_surface->GetScaledRect(),
|
||||
.dst_rect = surface_params.GetScaledRect(),
|
||||
};
|
||||
runtime.BlitTextures(*level_surface, *surface, texture_blit);
|
||||
watcher->Validate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return surface;
|
||||
return GetSurface(params, ScaleMatch::Ignore, true);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
@ -630,6 +569,7 @@ auto RasterizerCache<T>::GetTextureCube(const TextureCubeConfig& config) -> cons
|
||||
.width = config.width,
|
||||
.height = config.width,
|
||||
.stride = config.width,
|
||||
.levels = config.levels,
|
||||
.texture_type = TextureType::CubeMap,
|
||||
.pixel_format = PixelFormatFromTextureFormat(config.format),
|
||||
.type = SurfaceType::Texture,
|
||||
@ -640,53 +580,36 @@ auto RasterizerCache<T>::GetTextureCube(const TextureCubeConfig& config) -> cons
|
||||
|
||||
Surface& cube = it->second;
|
||||
|
||||
// Update surface watchers
|
||||
auto& watchers = cube->level_watchers;
|
||||
const u32 scaled_size = cube->GetScaledWidth();
|
||||
const std::array addresses = {config.px, config.nx, config.py, config.ny, config.pz, config.nz};
|
||||
|
||||
for (std::size_t i = 0; i < addresses.size(); i++) {
|
||||
auto& watcher = watchers[i];
|
||||
if (!watcher || !watcher->Get()) {
|
||||
Pica::Texture::TextureInfo info = {
|
||||
.physical_address = addresses[i],
|
||||
.width = config.width,
|
||||
.height = config.width,
|
||||
.format = config.format,
|
||||
};
|
||||
Pica::Texture::TextureInfo info = {
|
||||
.physical_address = addresses[i],
|
||||
.width = config.width,
|
||||
.height = config.width,
|
||||
.format = config.format,
|
||||
};
|
||||
info.SetDefaultStride();
|
||||
|
||||
info.SetDefaultStride();
|
||||
auto surface = GetTextureSurface(info);
|
||||
if (surface) {
|
||||
watcher = surface->CreateWatcher();
|
||||
} else {
|
||||
// Can occur when texture address is invalid. We mark the watcher with nullptr
|
||||
// in this case and the content of the face wouldn't get updated. These are usually
|
||||
// leftover setup in the texture unit and games are not supposed to draw using them.
|
||||
watcher = nullptr;
|
||||
}
|
||||
Surface face_surface = GetTextureSurface(info, config.levels - 1);
|
||||
if (!face_surface) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the face surfaces
|
||||
const u32 scaled_size = cube->GetScaledWidth();
|
||||
for (std::size_t i = 0; i < addresses.size(); i++) {
|
||||
const auto& watcher = watchers[i];
|
||||
if (watcher && !watcher->IsValid()) {
|
||||
auto face = std::static_pointer_cast<typename T::SurfaceType>(watcher->Get());
|
||||
if (!face->invalid_regions.empty()) {
|
||||
ValidateSurface(face, face->addr, face->size);
|
||||
}
|
||||
|
||||
const TextureBlit texture_blit = {
|
||||
.src_level = 0,
|
||||
.dst_level = 0,
|
||||
ASSERT(face_surface->levels == config.levels);
|
||||
const u32 face = static_cast<u32>(i);
|
||||
for (u32 level = 0; level < face_surface->levels; level++) {
|
||||
const TextureCopy texture_copy = {
|
||||
.src_level = level,
|
||||
.dst_level = level,
|
||||
.src_layer = 0,
|
||||
.dst_layer = static_cast<u32>(i),
|
||||
.src_rect = face->GetScaledRect(),
|
||||
.dst_rect = Rect2D{0, scaled_size, scaled_size, 0},
|
||||
.dst_layer = face,
|
||||
.src_offset = {0, 0},
|
||||
.dst_offset = {0, 0},
|
||||
.extent = {scaled_size >> level, scaled_size >> level},
|
||||
};
|
||||
runtime.BlitTextures(*face, *cube, texture_blit);
|
||||
watcher->Validate();
|
||||
runtime.CopyTextures(*face_surface, *cube, texture_copy);
|
||||
}
|
||||
}
|
||||
|
||||
@ -773,14 +696,16 @@ auto RasterizerCache<T>::GetFramebufferSurfaces(bool using_color_fb, bool using_
|
||||
}
|
||||
|
||||
if (color_surface) {
|
||||
ASSERT_MSG(color_surface->LevelOf(color_params.addr) == 0,
|
||||
"Rendering to mipmap of color surface unsupported");
|
||||
ValidateSurface(color_surface, boost::icl::first(color_vp_interval),
|
||||
boost::icl::length(color_vp_interval));
|
||||
color_surface->InvalidateAllWatcher();
|
||||
}
|
||||
if (depth_surface) {
|
||||
ASSERT_MSG(depth_surface->LevelOf(depth_params.addr) == 0,
|
||||
"Rendering to mipmap of depth surface unsupported");
|
||||
ValidateSurface(depth_surface, boost::icl::first(depth_vp_interval),
|
||||
boost::icl::length(depth_vp_interval));
|
||||
depth_surface->InvalidateAllWatcher();
|
||||
}
|
||||
|
||||
render_targets = RenderTargets{
|
||||
@ -874,63 +799,61 @@ void RasterizerCache<T>::ValidateSurface(const Surface& surface, PAddr addr, u32
|
||||
}
|
||||
|
||||
const SurfaceInterval validate_interval(addr, addr + size);
|
||||
const SurfaceRegions validate_regions = surface->invalid_regions & validate_interval;
|
||||
if (validate_regions.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill surfaces must always be valid when used
|
||||
if (surface->type == SurfaceType::Fill) {
|
||||
// Sanity check, fill surfaces will always be valid when used
|
||||
ASSERT(surface->IsRegionValid(validate_interval));
|
||||
return;
|
||||
}
|
||||
|
||||
auto validate_regions = surface->invalid_regions & validate_interval;
|
||||
for (u32 level = surface->LevelOf(addr); level <= surface->LevelOf(addr + size); level++) {
|
||||
auto level_regions = validate_regions & surface->LevelInterval(level);
|
||||
while (!level_regions.empty()) {
|
||||
const SurfaceInterval interval = *level_regions.begin();
|
||||
const SurfaceParams params = surface->FromInterval(interval);
|
||||
|
||||
const auto NotifyValidated = [&](SurfaceInterval interval) {
|
||||
surface->invalid_regions.erase(interval);
|
||||
validate_regions.erase(interval);
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const auto it = validate_regions.begin();
|
||||
if (it == validate_regions.end()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Look for a valid surface to copy from
|
||||
const auto interval = *it & validate_interval;
|
||||
SurfaceParams params = surface->FromInterval(interval);
|
||||
|
||||
Surface copy_surface = FindMatch<MatchFlags::Copy>(params, ScaleMatch::Ignore, interval);
|
||||
if (copy_surface != nullptr) {
|
||||
SurfaceInterval copy_interval = copy_surface->GetCopyableInterval(params);
|
||||
CopySurface(copy_surface, surface, copy_interval);
|
||||
NotifyValidated(copy_interval);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to find surface in cache with different format
|
||||
// that can can be reinterpreted to the requested format.
|
||||
if (ValidateByReinterpretation(surface, params, interval)) {
|
||||
NotifyValidated(interval);
|
||||
continue;
|
||||
}
|
||||
// Could not find a matching reinterpreter, check if we need to implement a
|
||||
// reinterpreter
|
||||
if (NoUnimplementedReinterpretations(surface, params, interval) &&
|
||||
!IntervalHasInvalidPixelFormat(params, interval)) {
|
||||
// No surfaces were found in the cache that had a matching bit-width.
|
||||
// If the region was created entirely on the GPU,
|
||||
// assume it was a developer mistake and skip flushing.
|
||||
if (boost::icl::contains(dirty_regions, interval)) {
|
||||
LOG_DEBUG(HW_GPU, "Region created fully on GPU and reinterpretation is "
|
||||
"invalid. Skipping validation");
|
||||
validate_regions.erase(interval);
|
||||
Surface copy_surface =
|
||||
FindMatch<MatchFlags::Copy>(params, ScaleMatch::Ignore, interval);
|
||||
if (copy_surface) {
|
||||
const SurfaceInterval copy_interval = copy_surface->GetCopyableInterval(params);
|
||||
CopySurface(copy_surface, surface, copy_interval);
|
||||
level_regions.erase(copy_interval);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Load data from 3DS memory
|
||||
FlushRegion(params.addr, params.size);
|
||||
UploadSurface(surface, interval);
|
||||
NotifyValidated(params.GetInterval());
|
||||
// Try to find surface in cache with different format
|
||||
// that can can be reinterpreted to the requested format.
|
||||
if (ValidateByReinterpretation(surface, params, interval)) {
|
||||
level_regions.erase(interval);
|
||||
continue;
|
||||
}
|
||||
// Could not find a matching reinterpreter, check if we need to implement a
|
||||
// reinterpreter
|
||||
if (NoUnimplementedReinterpretations(surface, params, interval) &&
|
||||
!IntervalHasInvalidPixelFormat(params, interval)) {
|
||||
// No surfaces were found in the cache that had a matching bit-width.
|
||||
// If the region was created entirely on the GPU,
|
||||
// assume it was a developer mistake and skip flushing.
|
||||
if (boost::icl::contains(dirty_regions, interval)) {
|
||||
LOG_DEBUG(HW_GPU, "Region created fully on GPU and reinterpretation is "
|
||||
"invalid. Skipping validation");
|
||||
level_regions.erase(interval);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Load data from 3DS memory
|
||||
FlushRegion(params.addr, params.size);
|
||||
UploadSurface(surface, interval);
|
||||
level_regions.erase(params.GetInterval());
|
||||
}
|
||||
}
|
||||
|
||||
surface->invalid_regions.erase(validate_interval);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
@ -956,7 +879,7 @@ void RasterizerCache<T>::UploadSurface(const Surface& surface, SurfaceInterval i
|
||||
.buffer_offset = 0,
|
||||
.buffer_size = staging.size,
|
||||
.texture_rect = surface->GetSubRect(load_info),
|
||||
.texture_level = 0,
|
||||
.texture_level = surface->LevelOf(load_info.addr),
|
||||
};
|
||||
surface->Upload(upload, staging);
|
||||
}
|
||||
@ -975,7 +898,7 @@ void RasterizerCache<T>::DownloadSurface(const Surface& surface, SurfaceInterval
|
||||
.buffer_offset = 0,
|
||||
.buffer_size = staging.size,
|
||||
.texture_rect = surface->GetSubRect(flush_info),
|
||||
.texture_level = 0,
|
||||
.texture_level = surface->LevelOf(flush_start),
|
||||
};
|
||||
surface->Download(download, staging);
|
||||
|
||||
@ -1025,7 +948,7 @@ void RasterizerCache<T>::DownloadFillSurface(const Surface& surface, SurfaceInte
|
||||
|
||||
template <class T>
|
||||
bool RasterizerCache<T>::NoUnimplementedReinterpretations(const Surface& surface,
|
||||
SurfaceParams& params,
|
||||
SurfaceParams params,
|
||||
SurfaceInterval interval) {
|
||||
static constexpr std::array all_formats = {
|
||||
PixelFormat::RGBA8, PixelFormat::RGB8, PixelFormat::RGB5A1, PixelFormat::RGB565,
|
||||
@ -1056,7 +979,7 @@ bool RasterizerCache<T>::NoUnimplementedReinterpretations(const Surface& surface
|
||||
}
|
||||
|
||||
template <class T>
|
||||
bool RasterizerCache<T>::IntervalHasInvalidPixelFormat(SurfaceParams& params,
|
||||
bool RasterizerCache<T>::IntervalHasInvalidPixelFormat(SurfaceParams params,
|
||||
SurfaceInterval interval) {
|
||||
bool invalid_format_found = false;
|
||||
ForEachSurfaceInRegion(params.addr, params.end, [&](Surface surface) {
|
||||
@ -1072,7 +995,7 @@ bool RasterizerCache<T>::IntervalHasInvalidPixelFormat(SurfaceParams& params,
|
||||
}
|
||||
|
||||
template <class T>
|
||||
bool RasterizerCache<T>::ValidateByReinterpretation(const Surface& surface, SurfaceParams& params,
|
||||
bool RasterizerCache<T>::ValidateByReinterpretation(const Surface& surface, SurfaceParams params,
|
||||
SurfaceInterval interval) {
|
||||
const PixelFormat dest_format = surface->pixel_format;
|
||||
for (const auto& reinterpreter : runtime.GetPossibleReinterpretations(dest_format)) {
|
||||
|
@ -157,14 +157,14 @@ private:
|
||||
void DownloadFillSurface(const Surface& surface, SurfaceInterval interval);
|
||||
|
||||
/// Returns false if there is a surface in the cache at the interval with the same bit-width,
|
||||
bool NoUnimplementedReinterpretations(const Surface& surface, SurfaceParams& params,
|
||||
bool NoUnimplementedReinterpretations(const Surface& surface, SurfaceParams params,
|
||||
SurfaceInterval interval);
|
||||
|
||||
/// Return true if a surface with an invalid pixel format exists at the interval
|
||||
bool IntervalHasInvalidPixelFormat(SurfaceParams& params, SurfaceInterval interval);
|
||||
bool IntervalHasInvalidPixelFormat(SurfaceParams params, SurfaceInterval interval);
|
||||
|
||||
/// Attempt to find a reinterpretable surface in the cache and use it to copy for validation
|
||||
bool ValidateByReinterpretation(const Surface& surface, SurfaceParams& params,
|
||||
bool ValidateByReinterpretation(const Surface& surface, SurfaceParams params,
|
||||
SurfaceInterval interval);
|
||||
|
||||
/// Create a new surface
|
||||
|
@ -151,30 +151,4 @@ std::array<u8, 4> SurfaceBase::MakeFillBuffer(PAddr copy_addr) {
|
||||
return fill_buffer;
|
||||
}
|
||||
|
||||
std::shared_ptr<Watcher> SurfaceBase::CreateWatcher() {
|
||||
auto weak_ptr = weak_from_this();
|
||||
auto watcher = std::make_shared<Watcher>(std::move(weak_ptr));
|
||||
watchers.push_back(watcher);
|
||||
return watcher;
|
||||
}
|
||||
|
||||
void SurfaceBase::InvalidateAllWatcher() {
|
||||
for (const auto& watcher : watchers) {
|
||||
if (auto locked = watcher.lock()) {
|
||||
locked->valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SurfaceBase::UnlinkAllWatcher() {
|
||||
for (const auto& watcher : watchers) {
|
||||
if (auto locked = watcher.lock()) {
|
||||
locked->valid = false;
|
||||
locked->surface.reset();
|
||||
}
|
||||
}
|
||||
|
||||
watchers.clear();
|
||||
}
|
||||
|
||||
} // namespace VideoCore
|
||||
|
@ -12,38 +12,7 @@ namespace VideoCore {
|
||||
|
||||
using SurfaceRegions = boost::icl::interval_set<PAddr, std::less, SurfaceInterval>;
|
||||
|
||||
class SurfaceBase;
|
||||
|
||||
/**
|
||||
* A watcher that notifies whether a cached surface has been changed. This is useful for caching
|
||||
* surface collection objects, including texture cube and mipmap.
|
||||
*/
|
||||
class Watcher {
|
||||
public:
|
||||
explicit Watcher(std::weak_ptr<SurfaceBase>&& surface) : surface(std::move(surface)) {}
|
||||
|
||||
/// Checks whether the surface has been changed.
|
||||
bool IsValid() const {
|
||||
return !surface.expired() && valid;
|
||||
}
|
||||
|
||||
/// Marks that the content of the referencing surface has been updated to the watcher user.
|
||||
void Validate() {
|
||||
ASSERT(!surface.expired());
|
||||
valid = true;
|
||||
}
|
||||
|
||||
/// Gets the referencing surface. Returns null if the surface has been destroyed
|
||||
std::shared_ptr<SurfaceBase> Get() const {
|
||||
return surface.lock();
|
||||
}
|
||||
|
||||
public:
|
||||
std::weak_ptr<SurfaceBase> surface;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
class SurfaceBase : public SurfaceParams, public std::enable_shared_from_this<SurfaceBase> {
|
||||
class SurfaceBase : public SurfaceParams {
|
||||
public:
|
||||
SurfaceBase();
|
||||
explicit SurfaceBase(const SurfaceParams& params);
|
||||
@ -66,15 +35,6 @@ public:
|
||||
/// Returns the clear value used to validate another surface from this fill surface
|
||||
ClearValue MakeClearValue(PAddr copy_addr, PixelFormat dst_format);
|
||||
|
||||
/// Creates a surface watcher linked to this surface
|
||||
std::shared_ptr<Watcher> CreateWatcher();
|
||||
|
||||
/// Invalidates all watchers linked to this surface
|
||||
void InvalidateAllWatcher();
|
||||
|
||||
/// Removes any linked watchers from this surface
|
||||
void UnlinkAllWatcher();
|
||||
|
||||
/// Returns true when the region denoted by interval is valid
|
||||
bool IsRegionValid(SurfaceInterval interval) const {
|
||||
return (invalid_regions.find(interval) == invalid_regions.end());
|
||||
@ -94,12 +54,8 @@ public:
|
||||
bool registered = false;
|
||||
bool picked = false;
|
||||
SurfaceRegions invalid_regions;
|
||||
std::array<std::shared_ptr<Watcher>, 7> level_watchers;
|
||||
std::array<u8, 4> fill_data;
|
||||
u32 fill_size = 0;
|
||||
|
||||
public:
|
||||
std::vector<std::weak_ptr<Watcher>> watchers;
|
||||
};
|
||||
|
||||
} // namespace VideoCore
|
||||
|
@ -56,24 +56,37 @@ void SurfaceParams::UpdateParams() {
|
||||
}
|
||||
|
||||
type = GetFormatType(pixel_format);
|
||||
size = !is_tiled ? BytesInPixels(stride * (height - 1) + width)
|
||||
: BytesInPixels(stride * 8 * (height / 8 - 1) + width * 8);
|
||||
if (levels != 1) {
|
||||
ASSERT(stride == width);
|
||||
CalculateMipLevelOffsets();
|
||||
size = CalculateSurfaceSize();
|
||||
} else {
|
||||
mipmap_offsets[0] = addr;
|
||||
size = !is_tiled ? BytesInPixels(stride * (height - 1) + width)
|
||||
: BytesInPixels(stride * 8 * (height / 8 - 1) + width * 8);
|
||||
}
|
||||
|
||||
end = addr + size;
|
||||
}
|
||||
|
||||
Rect2D SurfaceParams::GetSubRect(const SurfaceParams& sub_surface) const {
|
||||
const u32 begin_pixel_index = PixelsInBytes(sub_surface.addr - addr);
|
||||
const u32 level = LevelOf(sub_surface.addr);
|
||||
const u32 begin_pixel_index = PixelsInBytes(sub_surface.addr - mipmap_offsets[level]);
|
||||
ASSERT(stride == width || level == 0);
|
||||
|
||||
const u32 stride_lod = stride >> level;
|
||||
if (is_tiled) {
|
||||
const u32 x0 = (begin_pixel_index % (stride * 8)) / 8;
|
||||
const u32 y0 = (begin_pixel_index / (stride * 8)) * 8;
|
||||
const u32 x0 = (begin_pixel_index % (stride_lod * 8)) / 8;
|
||||
const u32 y0 = (begin_pixel_index / (stride_lod * 8)) * 8;
|
||||
const u32 height_lod = height >> level;
|
||||
|
||||
// Top to bottom
|
||||
return Rect2D(x0, height - y0, x0 + sub_surface.width, height - (y0 + sub_surface.height));
|
||||
return Rect2D(x0, height_lod - y0, x0 + sub_surface.width,
|
||||
height_lod - (y0 + sub_surface.height));
|
||||
}
|
||||
|
||||
const u32 x0 = begin_pixel_index % stride;
|
||||
const u32 y0 = begin_pixel_index / stride;
|
||||
const u32 x0 = begin_pixel_index % stride_lod;
|
||||
const u32 y0 = begin_pixel_index / stride_lod;
|
||||
// Bottom to top
|
||||
return Rect2D(x0, y0 + sub_surface.height, x0 + sub_surface.width, y0);
|
||||
}
|
||||
@ -85,26 +98,37 @@ Rect2D SurfaceParams::GetScaledSubRect(const SurfaceParams& sub_surface) const {
|
||||
|
||||
SurfaceParams SurfaceParams::FromInterval(SurfaceInterval interval) const {
|
||||
SurfaceParams params = *this;
|
||||
const u32 tiled_size = is_tiled ? 8 : 1;
|
||||
const u32 stride_tiled_bytes = BytesInPixels(stride * tiled_size);
|
||||
const u32 level = LevelOf(interval.lower());
|
||||
const PAddr end_addr = interval.upper();
|
||||
|
||||
// Ensure provided interval is contained in a single level
|
||||
ASSERT(level == LevelOf(end_addr) || end_addr == end || end_addr == mipmap_offsets[level + 1]);
|
||||
|
||||
params.width >>= level;
|
||||
params.stride >>= level;
|
||||
|
||||
const u32 tiled_size = is_tiled ? 8 : 1;
|
||||
const u32 stride_tiled_bytes = BytesInPixels(params.stride * tiled_size);
|
||||
ASSERT(stride == width || level == 0);
|
||||
|
||||
const PAddr start = mipmap_offsets[level];
|
||||
PAddr aligned_start =
|
||||
addr + Common::AlignDown(boost::icl::first(interval) - addr, stride_tiled_bytes);
|
||||
start + Common::AlignDown(boost::icl::first(interval) - start, stride_tiled_bytes);
|
||||
PAddr aligned_end =
|
||||
addr + Common::AlignUp(boost::icl::last_next(interval) - addr, stride_tiled_bytes);
|
||||
start + Common::AlignUp(boost::icl::last_next(interval) - start, stride_tiled_bytes);
|
||||
|
||||
if (aligned_end - aligned_start > stride_tiled_bytes) {
|
||||
params.addr = aligned_start;
|
||||
params.height = (aligned_end - aligned_start) / BytesInPixels(stride);
|
||||
params.height = (aligned_end - aligned_start) / BytesInPixels(params.stride);
|
||||
} else {
|
||||
// 1 row
|
||||
ASSERT(aligned_end - aligned_start == stride_tiled_bytes);
|
||||
const u32 tiled_alignment = BytesInPixels(is_tiled ? 8 * 8 : 1);
|
||||
|
||||
aligned_start =
|
||||
addr + Common::AlignDown(boost::icl::first(interval) - addr, tiled_alignment);
|
||||
start + Common::AlignDown(boost::icl::first(interval) - start, tiled_alignment);
|
||||
aligned_end =
|
||||
addr + Common::AlignUp(boost::icl::last_next(interval) - addr, tiled_alignment);
|
||||
start + Common::AlignUp(boost::icl::last_next(interval) - start, tiled_alignment);
|
||||
|
||||
params.addr = aligned_start;
|
||||
params.width = PixelsInBytes(aligned_end - aligned_start) / tiled_size;
|
||||
@ -112,11 +136,13 @@ SurfaceParams SurfaceParams::FromInterval(SurfaceInterval interval) const {
|
||||
params.height = tiled_size;
|
||||
}
|
||||
|
||||
params.levels = 1;
|
||||
params.UpdateParams();
|
||||
return params;
|
||||
}
|
||||
|
||||
SurfaceInterval SurfaceParams::GetSubRectInterval(Rect2D unscaled_rect) const {
|
||||
ASSERT(levels == 1);
|
||||
if (unscaled_rect.GetHeight() == 0 || unscaled_rect.GetWidth() == 0) [[unlikely]] {
|
||||
return {};
|
||||
}
|
||||
@ -137,4 +163,52 @@ SurfaceInterval SurfaceParams::GetSubRectInterval(Rect2D unscaled_rect) const {
|
||||
return {addr + BytesInPixels(pixel_offset), addr + BytesInPixels(pixel_offset + pixels)};
|
||||
}
|
||||
|
||||
void SurfaceParams::CalculateMipLevelOffsets() {
|
||||
ASSERT(levels <= MAX_PICA_LEVELS && stride == width);
|
||||
|
||||
u32 level_width = width;
|
||||
u32 level_height = height;
|
||||
u32 offset = addr;
|
||||
|
||||
for (u32 level = 0; level < levels; level++) {
|
||||
mipmap_offsets[level] = offset;
|
||||
offset += BytesInPixels(level_width * level_height);
|
||||
|
||||
level_width >>= 1;
|
||||
level_height >>= 1;
|
||||
}
|
||||
}
|
||||
|
||||
u32 SurfaceParams::CalculateSurfaceSize() const {
|
||||
ASSERT(levels <= MAX_PICA_LEVELS && stride == width);
|
||||
|
||||
u32 level_width = width;
|
||||
u32 level_height = height;
|
||||
u32 size = 0;
|
||||
|
||||
for (u32 level = 0; level < levels; level++) {
|
||||
size += BytesInPixels(level_width * level_height);
|
||||
level_width >>= 1;
|
||||
level_height >>= 1;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
SurfaceInterval SurfaceParams::LevelInterval(u32 level) const {
|
||||
ASSERT(levels > level);
|
||||
const PAddr start_addr = mipmap_offsets[level];
|
||||
const PAddr end_addr = level == (levels - 1) ? end : mipmap_offsets[level + 1];
|
||||
return {start_addr, end_addr};
|
||||
}
|
||||
|
||||
u32 SurfaceParams::LevelOf(PAddr level_addr) const {
|
||||
ASSERT(level_addr >= addr && level_addr <= end);
|
||||
|
||||
u32 level = levels - 1;
|
||||
while (mipmap_offsets[level] > level_addr) {
|
||||
level--;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
} // namespace VideoCore
|
||||
|
@ -8,6 +8,8 @@
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
constexpr std::size_t MAX_PICA_LEVELS = 8;
|
||||
|
||||
class SurfaceParams {
|
||||
public:
|
||||
/// Returns true if other_surface matches exactly params
|
||||
@ -37,6 +39,12 @@ public:
|
||||
/// Returns the address interval referenced by unscaled_rect
|
||||
SurfaceInterval GetSubRectInterval(Rect2D unscaled_rect) const;
|
||||
|
||||
/// Return the address interval of the provided level
|
||||
SurfaceInterval LevelInterval(u32 level) const;
|
||||
|
||||
/// Returns the level of the provided address
|
||||
u32 LevelOf(PAddr addr) const;
|
||||
|
||||
[[nodiscard]] SurfaceInterval GetInterval() const noexcept {
|
||||
return SurfaceInterval{addr, end};
|
||||
}
|
||||
@ -69,6 +77,13 @@ public:
|
||||
return pixels * GetFormatBpp() / 8;
|
||||
}
|
||||
|
||||
private:
|
||||
/// Computes the offset of each mipmap level
|
||||
void CalculateMipLevelOffsets();
|
||||
|
||||
/// Calculates total surface size taking mipmaps into account
|
||||
u32 CalculateSurfaceSize() const;
|
||||
|
||||
public:
|
||||
PAddr addr = 0;
|
||||
PAddr end = 0;
|
||||
@ -84,6 +99,8 @@ public:
|
||||
TextureType texture_type = TextureType::Texture2D;
|
||||
PixelFormat pixel_format = PixelFormat::Invalid;
|
||||
SurfaceType type = SurfaceType::Invalid;
|
||||
|
||||
std::array<u32, MAX_PICA_LEVELS> mipmap_offsets{};
|
||||
};
|
||||
|
||||
} // namespace VideoCore
|
||||
|
@ -99,6 +99,7 @@ struct TextureCubeConfig {
|
||||
PAddr pz;
|
||||
PAddr nz;
|
||||
u32 width;
|
||||
u32 levels;
|
||||
Pica::TexturingRegs::TextureFormat format;
|
||||
|
||||
auto operator<=>(const TextureCubeConfig&) const noexcept = default;
|
||||
|
@ -65,6 +65,7 @@ static void APIENTRY DebugHandler(GLenum source, GLenum type, GLuint id, GLenum
|
||||
level = Log::Level::Debug;
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_GENERIC(Log::Class::Render_OpenGL, level, "{} {} {}: {}", GetSource(source), GetType(type),
|
||||
id, message);
|
||||
}
|
||||
|
@ -452,6 +452,7 @@ bool RasterizerOpenGL::Draw(bool accelerate, bool is_indexed) {
|
||||
.pz = regs.texturing.GetCubePhysicalAddress(CubeFace::PositiveZ),
|
||||
.nz = regs.texturing.GetCubePhysicalAddress(CubeFace::NegativeZ),
|
||||
.width = texture.config.width,
|
||||
.levels = texture.config.lod.max_level + 1,
|
||||
.format = texture.format};
|
||||
|
||||
state.texture_cube_unit.texture_cube =
|
||||
|
@ -16,6 +16,8 @@
|
||||
|
||||
namespace OpenGL {
|
||||
|
||||
using VideoCore::TextureType;
|
||||
|
||||
constexpr FormatTuple DEFAULT_TUPLE = {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE};
|
||||
|
||||
static constexpr std::array DEPTH_TUPLES = {
|
||||
@ -137,10 +139,6 @@ OGLTexture TextureRuntime::Allocate(u32 width, u32 height, u32 levels,
|
||||
return texture;
|
||||
}
|
||||
|
||||
const auto& tuple = GetFormatTuple(format);
|
||||
const OpenGLState& state = OpenGLState::GetCurState();
|
||||
GLuint old_tex = state.texture_units[0].texture_2d;
|
||||
|
||||
// Allocate new texture
|
||||
OGLTexture texture{};
|
||||
texture.Create();
|
||||
@ -148,13 +146,14 @@ OGLTexture TextureRuntime::Allocate(u32 width, u32 height, u32 levels,
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(target, texture.handle);
|
||||
|
||||
const auto& tuple = GetFormatTuple(format);
|
||||
glTexStorage2D(target, levels, tuple.internal_format, width, height);
|
||||
|
||||
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
|
||||
glBindTexture(target, old_tex);
|
||||
glBindTexture(target, OpenGLState::GetCurState().texture_units[0].texture_2d);
|
||||
return texture;
|
||||
}
|
||||
|
||||
@ -221,10 +220,14 @@ bool TextureRuntime::ClearTexture(Surface& surface, const VideoCore::TextureClea
|
||||
|
||||
bool TextureRuntime::CopyTextures(Surface& source, Surface& dest,
|
||||
const VideoCore::TextureCopy& copy) {
|
||||
glCopyImageSubData(source.texture.handle, GL_TEXTURE_2D, copy.src_level, copy.src_offset.x,
|
||||
copy.src_offset.y, 0, dest.texture.handle, GL_TEXTURE_2D, copy.dst_level,
|
||||
copy.dst_offset.x, copy.dst_offset.y, 0, copy.extent.width,
|
||||
copy.extent.height, 1);
|
||||
const GLenum src_textarget =
|
||||
source.texture_type == TextureType::CubeMap ? GL_TEXTURE_CUBE_MAP : GL_TEXTURE_2D;
|
||||
const GLenum dst_textarget =
|
||||
dest.texture_type == TextureType::CubeMap ? GL_TEXTURE_CUBE_MAP : GL_TEXTURE_2D;
|
||||
glCopyImageSubData(source.texture.handle, src_textarget, copy.src_level, copy.src_offset.x,
|
||||
copy.src_offset.y, copy.src_layer, dest.texture.handle, dst_textarget,
|
||||
copy.dst_level, copy.dst_offset.x, copy.dst_offset.y, copy.dst_layer,
|
||||
copy.extent.width, copy.extent.height, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -238,13 +241,13 @@ bool TextureRuntime::BlitTextures(Surface& source, Surface& dest,
|
||||
state.draw.draw_framebuffer = draw_fbo.handle;
|
||||
state.Apply();
|
||||
|
||||
const GLenum src_textarget = source.texture_type == VideoCore::TextureType::CubeMap
|
||||
const GLenum src_textarget = source.texture_type == TextureType::CubeMap
|
||||
? GL_TEXTURE_CUBE_MAP_POSITIVE_X + blit.src_layer
|
||||
: GL_TEXTURE_2D;
|
||||
BindFramebuffer(GL_READ_FRAMEBUFFER, blit.src_level, src_textarget, source.type,
|
||||
source.texture);
|
||||
|
||||
const GLenum dst_textarget = dest.texture_type == VideoCore::TextureType::CubeMap
|
||||
const GLenum dst_textarget = dest.texture_type == TextureType::CubeMap
|
||||
? GL_TEXTURE_CUBE_MAP_POSITIVE_X + blit.dst_layer
|
||||
: GL_TEXTURE_2D;
|
||||
BindFramebuffer(GL_DRAW_FRAMEBUFFER, blit.dst_level, dst_textarget, dest.type, dest.texture);
|
||||
@ -347,7 +350,8 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, const StagingDa
|
||||
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||
SCOPE_EXIT({ prev_state.Apply(); });
|
||||
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(stride));
|
||||
const VideoCore::Rect2D rect = upload.texture_rect;
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(rect.GetWidth()));
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, staging.buffer);
|
||||
|
||||
// Unmap the buffer FindStaging mapped beforehand
|
||||
@ -357,15 +361,12 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, const StagingDa
|
||||
glBindTexture(GL_TEXTURE_2D, texture.handle);
|
||||
|
||||
const auto& tuple = runtime.GetFormatTuple(pixel_format);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, upload.texture_level, upload.texture_rect.left,
|
||||
upload.texture_rect.bottom, upload.texture_rect.GetWidth(),
|
||||
upload.texture_rect.GetHeight(), tuple.format, tuple.type,
|
||||
glTexSubImage2D(GL_TEXTURE_2D, upload.texture_level, rect.left, rect.bottom,
|
||||
rect.GetWidth(), rect.GetHeight(), tuple.format, tuple.type,
|
||||
reinterpret_cast<void*>(staging.buffer_offset));
|
||||
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
}
|
||||
|
||||
InvalidateAllWatcher();
|
||||
}
|
||||
|
||||
MICROPROFILE_DEFINE(OpenGL_Download, "OpenGL", "Texture Download", MP_RGB(128, 192, 64));
|
||||
@ -375,25 +376,25 @@ void Surface::Download(const VideoCore::BufferTextureCopy& download, const Stagi
|
||||
// Ensure no bad interactions with GL_PACK_ALIGNMENT
|
||||
ASSERT(stride * GetBytesPerPixel(pixel_format) % 4 == 0);
|
||||
|
||||
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||
SCOPE_EXIT({ prev_state.Apply(); });
|
||||
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, static_cast<GLint>(stride));
|
||||
|
||||
const bool is_scaled = res_scale != 1;
|
||||
if (is_scaled) {
|
||||
ScaledDownload(download, staging);
|
||||
} else {
|
||||
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||
SCOPE_EXIT({ prev_state.Apply(); });
|
||||
|
||||
const VideoCore::Rect2D rect = download.texture_rect;
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, static_cast<GLint>(rect.GetWidth()));
|
||||
|
||||
runtime.BindFramebuffer(GL_READ_FRAMEBUFFER, download.texture_level, GL_TEXTURE_2D, type,
|
||||
texture);
|
||||
|
||||
const auto& tuple = runtime.GetFormatTuple(pixel_format);
|
||||
glReadPixels(download.texture_rect.left, download.texture_rect.bottom,
|
||||
download.texture_rect.GetWidth(), download.texture_rect.GetHeight(),
|
||||
tuple.format, tuple.type, staging.mapped.data());
|
||||
}
|
||||
glReadPixels(rect.left, rect.bottom, rect.GetWidth(), rect.GetHeight(), tuple.format,
|
||||
tuple.type, staging.mapped.data());
|
||||
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Surface::ScaledUpload(const VideoCore::BufferTextureCopy& upload, const StagingData& staging) {
|
||||
@ -414,7 +415,6 @@ void Surface::ScaledUpload(const VideoCore::BufferTextureCopy& upload, const Sta
|
||||
.buffer_size = upload.buffer_size,
|
||||
.texture_rect = unscaled_rect,
|
||||
};
|
||||
|
||||
unscaled_surface.Upload(unscaled_upload, staging);
|
||||
|
||||
const auto& filterer = runtime.GetFilterer();
|
||||
@ -422,13 +422,9 @@ void Surface::ScaledUpload(const VideoCore::BufferTextureCopy& upload, const Sta
|
||||
const VideoCore::TextureBlit blit = {
|
||||
.src_level = 0,
|
||||
.dst_level = upload.texture_level,
|
||||
.src_layer = 0,
|
||||
.dst_layer = 0,
|
||||
.src_rect = unscaled_rect,
|
||||
.dst_rect = scaled_rect,
|
||||
};
|
||||
|
||||
// If filtering fails, resort to normal blitting
|
||||
runtime.BlitTextures(unscaled_surface, *this, blit);
|
||||
}
|
||||
}
|
||||
@ -451,7 +447,7 @@ void Surface::ScaledDownload(const VideoCore::BufferTextureCopy& download,
|
||||
// Blit the scaled rectangle to the unscaled texture
|
||||
const VideoCore::TextureBlit blit = {
|
||||
.src_level = download.texture_level,
|
||||
.dst_level = 0,
|
||||
.dst_level = download.texture_level,
|
||||
.src_layer = 0,
|
||||
.dst_layer = 0,
|
||||
.src_rect = scaled_rect,
|
||||
@ -464,12 +460,13 @@ void Surface::ScaledDownload(const VideoCore::BufferTextureCopy& download,
|
||||
|
||||
const auto& tuple = runtime.GetFormatTuple(pixel_format);
|
||||
if (driver.IsOpenGLES()) {
|
||||
runtime.BindFramebuffer(GL_READ_FRAMEBUFFER, 0, GL_TEXTURE_2D, type,
|
||||
runtime.BindFramebuffer(GL_READ_FRAMEBUFFER, download.texture_level, GL_TEXTURE_2D, type,
|
||||
unscaled_surface.texture);
|
||||
glReadPixels(0, 0, rect_width, rect_height, tuple.format, tuple.type,
|
||||
staging.mapped.data());
|
||||
} else {
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, tuple.format, tuple.type, staging.mapped.data());
|
||||
glGetTexImage(GL_TEXTURE_2D, download.texture_level, tuple.format, tuple.type,
|
||||
staging.mapped.data());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -618,6 +618,7 @@ void RasterizerVulkan::SyncTextureUnits(const Framebuffer& framebuffer) {
|
||||
.pz = regs.texturing.GetCubePhysicalAddress(CubeFace::PositiveZ),
|
||||
.nz = regs.texturing.GetCubePhysicalAddress(CubeFace::NegativeZ),
|
||||
.width = texture.config.width,
|
||||
.levels = texture.config.lod.max_level + 1,
|
||||
.format = texture.format};
|
||||
|
||||
auto surface = res_cache.GetTextureCube(config);
|
||||
|
@ -871,8 +871,6 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, const StagingDa
|
||||
|
||||
runtime.upload_buffer.Commit(staging.size);
|
||||
}
|
||||
|
||||
InvalidateAllWatcher();
|
||||
}
|
||||
|
||||
void Surface::Download(const VideoCore::BufferTextureCopy& download, const StagingData& staging) {
|
||||
|
Reference in New Issue
Block a user