| // Copyright 2012 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "gpu/command_buffer/service/command_buffer_service.h" | |
| #include <stddef.h> | |
| #include <stdint.h> | |
| #include <limits> | |
| #include <memory> | |
| #include "base/compiler_specific.h" | |
| #include "base/logging.h" | |
| #include "base/memory/page_size.h" | |
| #include "base/task/single_thread_task_runner.h" | |
| #include "base/trace_event/memory_dump_manager.h" | |
| #include "base/trace_event/memory_dump_provider.h" | |
| #include "base/trace_event/memory_dump_request_args.h" | |
| #include "base/trace_event/trace_event.h" | |
| #include "build/build_config.h" | |
| #include "gpu/command_buffer/common/cmd_buffer_common.h" | |
| #include "gpu/command_buffer/common/command_buffer_shared.h" | |
| #include "gpu/command_buffer/service/memory_tracking.h" | |
| #include "gpu/command_buffer/service/transfer_buffer_manager.h" | |
| #include "gpu/config/gpu_finch_features.h" | |
| #if BUILDFLAG(IS_MAC) | |
| #include <mach/mach_vm.h> | |
| #include <mach/vm_purgable.h> | |
| #include <mach/vm_statistics.h> | |
| #include "base/no_destructor.h" | |
| #include "base/process/process_metrics.h" | |
| #include "base/trace_event/process_memory_dump.h" | |
| #include "ui/gl/gl_display.h" | |
| #include "ui/gl/gl_implementation.h" | |
| #include "ui/gl/gl_utils.h" | |
| #endif | |
| namespace gpu { | |
| #if BUILDFLAG(IS_MAC) | |
| namespace { | |
| class AppleGpuMemoryDumpProvider | |
| : public base::trace_event::MemoryDumpProvider { | |
| public: | |
| AppleGpuMemoryDumpProvider(); | |
| bool OnMemoryDump(const base::trace_event::MemoryDumpArgs& args, | |
| base::trace_event::ProcessMemoryDump* pmd) override; | |
| private: | |
| // NoDestructor only. | |
| ~AppleGpuMemoryDumpProvider() override = default; | |
| }; | |
| AppleGpuMemoryDumpProvider::AppleGpuMemoryDumpProvider() { | |
| base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider( | |
| this, "CommandBuffer", nullptr); | |
| } | |
| bool AppleGpuMemoryDumpProvider::OnMemoryDump( | |
| const base::trace_event::MemoryDumpArgs& args, | |
| base::trace_event::ProcessMemoryDump* pmd) { | |
| // Collect IOSurface total memory usage. | |
| size_t surface_virtual_size = 0; | |
| size_t surface_resident_size = 0; | |
| size_t surface_swapped_out_size = 0; | |
| size_t surface_dirty_size = 0; | |
| size_t surface_nonpurgeable_size = 0; | |
| size_t surface_purgeable_size = 0; | |
| // And IOAccelerator. Per vm_statistics.h in XNU, this is used to | |
| // "differentiate memory needed by GPU drivers and frameworks from generic | |
| // IOKit allocations". See xnu-1456.1.26/osfmk/mach/vm_statistics.h. | |
| size_t accelerator_virtual_size = 0; | |
| size_t accelerator_resident_size = 0; | |
| size_t accelerator_swapped_out_size = 0; | |
| size_t accelerator_dirty_size = 0; | |
| size_t accelerator_nonpurgeable_size = 0; | |
| size_t accelerator_purgeable_size = 0; | |
| task_t task = mach_task_self(); | |
| mach_vm_address_t address = 0; | |
| mach_vm_size_t size = 0; | |
| while (true) { | |
| address += size; | |
| // GetBasicInfo is faster than querying the extended attributes. Query this | |
| // first to filter out regions that cannot correspond to IOSurfaces. | |
| vm_region_basic_info_64 basic_info; | |
| base::MachVMRegionResult result = | |
| base::GetBasicInfo(task, &size, &address, &basic_info); | |
| if (result == base::MachVMRegionResult::Finished) { | |
| break; | |
| } else if (result == base::MachVMRegionResult::Error) { | |
| return false; | |
| } | |
| // All IOSurfaces and IOAccelerator allocations seen locally (M1 laptop) | |
| // have rw-/rw- permissions. More distinctive characteristics require the | |
| // extended info, which are more expensive to query. | |
| const vm_prot_t rw = VM_PROT_READ | VM_PROT_WRITE; | |
| if (basic_info.protection != rw || basic_info.max_protection != rw) | |
| continue; | |
| // Candidate, need the extended info to get the user tag, but also the page | |
| // status breakdown. | |
| vm_region_extended_info_data_t info; | |
| mach_port_t object_name; | |
| mach_msg_type_number_t count; | |
| count = VM_REGION_EXTENDED_INFO_COUNT; | |
| kern_return_t ret = mach_vm_region( | |
| task, &address, &size, VM_REGION_EXTENDED_INFO, | |
| reinterpret_cast<vm_region_info_t>(&info), &count, &object_name); | |
| // No regions above the requested address. | |
| if (ret == KERN_INVALID_ADDRESS) | |
| break; | |
| if (ret != KERN_SUCCESS) | |
| return false; | |
| if (info.user_tag != VM_MEMORY_IOSURFACE && | |
| info.user_tag != VM_MEMORY_IOACCELERATOR) { | |
| continue; | |
| } | |
| int purgeable_state = 0; | |
| ret = mach_vm_purgable_control(task, address, VM_PURGABLE_GET_STATE, | |
| &purgeable_state); | |
| purgeable_state = purgeable_state & VM_PURGABLE_STATE_MASK; | |
| switch (info.user_tag) { | |
| case VM_MEMORY_IOSURFACE: | |
| surface_virtual_size += size; | |
| surface_resident_size += info.pages_resident * base::GetPageSize(); | |
| surface_swapped_out_size += | |
| info.pages_swapped_out * base::GetPageSize(); | |
| surface_dirty_size += info.pages_dirtied * base::GetPageSize(); | |
| if (purgeable_state == VM_PURGABLE_VOLATILE || | |
| purgeable_state == VM_PURGABLE_EMPTY) { | |
| surface_purgeable_size += size; | |
| } else { | |
| surface_nonpurgeable_size += size; | |
| } | |
| break; | |
| case VM_MEMORY_IOACCELERATOR: | |
| accelerator_virtual_size += size; | |
| accelerator_resident_size += info.pages_resident * base::GetPageSize(); | |
| accelerator_swapped_out_size += | |
| info.pages_swapped_out * base::GetPageSize(); | |
| accelerator_dirty_size += info.pages_dirtied * base::GetPageSize(); | |
| if (purgeable_state == VM_PURGABLE_VOLATILE || | |
| purgeable_state == VM_PURGABLE_EMPTY) { | |
| accelerator_purgeable_size += size; | |
| } else { | |
| accelerator_nonpurgeable_size += size; | |
| } | |
| break; | |
| } | |
| } | |
| auto* dump = pmd->CreateAllocatorDump("iosurface"); | |
| dump->AddScalar("virtual_size", "bytes", surface_virtual_size); | |
| dump->AddScalar("resident_size", "bytes", surface_resident_size); | |
| dump->AddScalar("swapped_out_size", "bytes", surface_swapped_out_size); | |
| dump->AddScalar("dirty_size", "bytes", surface_dirty_size); | |
| dump->AddScalar("size", "bytes", surface_virtual_size); | |
| // Some IOSurfaces have a non-trivial difference between their mapped size | |
| // and their "dirty" size, possibly because some of it has been marked | |
| // purgeable, and has been purged (rather than swapped out). Report resident | |
| // + swapped, as it is the fraction of memory which is: (a) using actual | |
| // memory, and (b) counted in private memory footprint. | |
| // | |
| // Note: not using "dirty_size", as it doesn't contain the swapped out part. | |
| dump->AddScalar("resident_swapped", "bytes", | |
| surface_resident_size + surface_swapped_out_size); | |
| dump->AddScalar("nonpurgeable_size", "bytes", surface_nonpurgeable_size); | |
| dump->AddScalar("purgeable_size", "bytes", surface_purgeable_size); | |
| // Ditto for IOAccelerator. | |
| dump = pmd->CreateAllocatorDump("ioaccelerator"); | |
| dump->AddScalar("virtual_size", "bytes", accelerator_virtual_size); | |
| dump->AddScalar("resident_size", "bytes", accelerator_resident_size); | |
| dump->AddScalar("swapped_out_size", "bytes", accelerator_swapped_out_size); | |
| dump->AddScalar("dirty_size", "bytes", accelerator_dirty_size); | |
| dump->AddScalar("size", "bytes", accelerator_virtual_size); | |
| dump->AddScalar("resident_swapped", "bytes", | |
| accelerator_resident_size + accelerator_swapped_out_size); | |
| dump->AddScalar("nonpurgeable_size", "bytes", accelerator_nonpurgeable_size); | |
| dump->AddScalar("purgeable_size", "bytes", accelerator_purgeable_size); | |
| if (gl::GetANGLEImplementation() == gl::ANGLEImplementation::kMetal) { | |
| gl::GLDisplayEGL* display_egl = gl::GetDefaultDisplayEGL(); | |
| if (display_egl) { | |
| dump = pmd->CreateAllocatorDump("gpu/angle/metal"); | |
| dump->AddScalar("size", "bytes", | |
| display_egl->GetMetalDeviceAllocatedMemory()); | |
| } | |
| } | |
| return true; | |
| } | |
| } // namespace | |
| #endif | |
| // Context switching leads to a render pass break in ANGLE/Vulkan. The command | |
| // buffer has a 20-command limit before it forces a context switch. This | |
| // experiment tests a 100-command limit. | |
| int GetCommandBufferSliceSize() { | |
| static int slice_size = | |
| (base::FeatureList::IsEnabled(features::kIncreasedCmdBufferParseSlice) | |
| ? CommandBufferService::kParseCommandsSliceLarge | |
| : CommandBufferService::kParseCommandsSliceSmall); | |
| return slice_size; | |
| } | |
| CommandBufferService::CommandBufferService( | |
| CommandBufferServiceClient* client, | |
| scoped_refptr<MemoryTracker> memory_tracker) | |
| : client_(client), | |
| transfer_buffer_manager_( | |
| std::make_unique<TransferBufferManager>(std::move(memory_tracker))) { | |
| DCHECK(client_); | |
| state_.token = 0; | |
| #if BUILDFLAG(IS_MAC) | |
| static base::NoDestructor<AppleGpuMemoryDumpProvider> dump_provider; | |
| #endif | |
| } | |
| CommandBufferService::~CommandBufferService() = default; | |
| void CommandBufferService::UpdateState() { | |
| ++state_.generation; | |
| if (shared_state_) | |
| shared_state_->Write(state_); | |
| } | |
| void CommandBufferService::Flush(int32_t put_offset, | |
| AsyncAPIInterface* handler) { | |
| DCHECK(handler); | |
| if (put_offset < 0 || put_offset >= num_entries_) { | |
| SetParseError(gpu::error::kOutOfBounds); | |
| return; | |
| } | |
| TRACE_EVENT2("gpu", "CommandBufferService:PutChanged", "handler", | |
| std::string(handler->GetLogPrefix()), "put_offset", put_offset); | |
| put_offset_ = put_offset; | |
| DCHECK(buffer_); | |
| if (state_.error != error::kNoError) | |
| return; | |
| DCHECK(scheduled()); | |
| if (paused_) { | |
| paused_ = false; | |
| TRACE_COUNTER_ID1("gpu", "CommandBufferService::Paused", this, paused_); | |
| } | |
| handler->BeginDecoding(); | |
| // BeginDecoding can cause context loss due to resuming shared image access. | |
| if (state_.error != error::kNoError) { | |
| handler->EndDecoding(); | |
| return; | |
| } | |
| int end = put_offset_ < state_.get_offset ? num_entries_ : put_offset_; | |
| while (put_offset_ != state_.get_offset) { | |
| int num_entries = end - state_.get_offset; | |
| int entries_processed = 0; | |
| error::Error error = handler->DoCommands( | |
| GetCommandBufferSliceSize(), UNSAFE_TODO(buffer_ + state_.get_offset), | |
| num_entries, &entries_processed); | |
| state_.get_offset += entries_processed; | |
| DCHECK_LE(state_.get_offset, num_entries_); | |
| if (state_.get_offset == num_entries_) { | |
| end = put_offset_; | |
| state_.get_offset = 0; | |
| } | |
| if (error::IsError(error)) { | |
| SetParseError(error); | |
| break; | |
| } | |
| if (client_->OnCommandBatchProcessed() == | |
| CommandBufferServiceClient::kPauseExecution) { | |
| paused_ = true; | |
| TRACE_COUNTER_ID1("gpu", "CommandBufferService::Paused", this, paused_); | |
| break; | |
| } | |
| if (!scheduled()) | |
| break; | |
| } | |
| handler->EndDecoding(); | |
| } | |
| void CommandBufferService::SetGetBuffer(int32_t transfer_buffer_id) { | |
| DCHECK((put_offset_ == state_.get_offset) || | |
| (state_.error != error::kNoError)); | |
| put_offset_ = 0; | |
| state_.get_offset = 0; | |
| ++state_.set_get_buffer_count; | |
| // If the buffer is invalid we handle it gracefully. | |
| // This means `transfer_buffer` can be nullptr. | |
| auto transfer_buffer = GetTransferBuffer(transfer_buffer_id); | |
| if (transfer_buffer) { | |
| uint32_t size = transfer_buffer->size(); | |
| volatile void* memory = transfer_buffer->memory(); | |
| // check proper alignments. | |
| DCHECK_EQ( | |
| 0u, (reinterpret_cast<intptr_t>(memory)) % alignof(CommandBufferEntry)); | |
| DCHECK_EQ(0u, size % sizeof(CommandBufferEntry)); | |
| num_entries_ = size / sizeof(CommandBufferEntry); | |
| buffer_ = reinterpret_cast<volatile CommandBufferEntry*>(memory); | |
| } else { | |
| num_entries_ = 0; | |
| buffer_ = nullptr; | |
| } | |
| ring_buffer_ = std::move(transfer_buffer); | |
| UpdateState(); | |
| } | |
| void CommandBufferService::SetSharedStateBuffer( | |
| std::unique_ptr<BufferBacking> shared_state_buffer) { | |
| shared_state_buffer_ = std::move(shared_state_buffer); | |
| DCHECK(shared_state_buffer_->GetSize() >= sizeof(*shared_state_)); | |
| shared_state_ = | |
| static_cast<CommandBufferSharedState*>(shared_state_buffer_->GetMemory()); | |
| UpdateState(); | |
| } | |
| CommandBuffer::State CommandBufferService::GetState() { | |
| return state_; | |
| } | |
| void CommandBufferService::SetReleaseCount(uint64_t release_count) { | |
| DLOG_IF(ERROR, release_count < state_.release_count) | |
| << "Non-monotonic SetReleaseCount"; | |
| state_.release_count = release_count; | |
| UpdateState(); | |
| } | |
| scoped_refptr<Buffer> CommandBufferService::CreateTransferBuffer( | |
| uint32_t size, | |
| int32_t* id, | |
| uint32_t alignment) { | |
| *id = GetNextBufferId(); | |
| auto result = CreateTransferBufferWithId(size, *id, alignment); | |
| if (!result) { | |
| *id = -1; | |
| } | |
| return result; | |
| } | |
| void CommandBufferService::DestroyTransferBuffer(int32_t id) { | |
| transfer_buffer_manager_->DestroyTransferBuffer(id); | |
| } | |
| scoped_refptr<Buffer> CommandBufferService::GetTransferBuffer(int32_t id) { | |
| return transfer_buffer_manager_->GetTransferBuffer(id); | |
| } | |
| bool CommandBufferService::RegisterTransferBuffer( | |
| int32_t id, | |
| scoped_refptr<Buffer> buffer) { | |
| return transfer_buffer_manager_->RegisterTransferBuffer(id, | |
| std::move(buffer)); | |
| } | |
| scoped_refptr<Buffer> CommandBufferService::CreateTransferBufferWithId( | |
| uint32_t size, | |
| int32_t id, | |
| uint32_t alignment) { | |
| scoped_refptr<Buffer> buffer = MakeMemoryBuffer(size, alignment); | |
| if (!RegisterTransferBuffer(id, buffer)) { | |
| SetParseError(gpu::error::kOutOfBounds); | |
| return nullptr; | |
| } | |
| return buffer; | |
| } | |
| void CommandBufferService::SetToken(int32_t token) { | |
| state_.token = token; | |
| UpdateState(); | |
| } | |
| void CommandBufferService::SetParseError(error::Error error) { | |
| if (state_.error == error::kNoError) { | |
| state_.error = error; | |
| client_->OnParseError(); | |
| } | |
| } | |
| void CommandBufferService::SetContextLostReason( | |
| error::ContextLostReason reason) { | |
| state_.context_lost_reason = reason; | |
| } | |
| void CommandBufferService::SetScheduled(bool scheduled) { | |
| TRACE_EVENT2("gpu", "CommandBufferService:SetScheduled", "this", | |
| static_cast<void*>(this), "scheduled", scheduled); | |
| scheduled_ = scheduled; | |
| } | |
| size_t CommandBufferService::GetSharedMemoryBytesAllocated() const { | |
| return transfer_buffer_manager_->shared_memory_bytes_allocated(); | |
| } | |
| } // namespace gpu |