| // Copyright 2022 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "vm_tools/cicerone/shadercached_helper.h" | |
| #include <memory> | |
| #include <utility> | |
| #include <base/logging.h> | |
| #include <base/strings/stringprintf.h> | |
| #include <base/synchronization/waitable_event.h> | |
| #include <dbus/message.h> | |
| #include <dbus/error.h> | |
| #include <dbus/shadercached/dbus-constants.h> | |
| #include <dbus/dlcservice/dbus-constants.h> | |
| #include <dlcservice/proto_bindings/dlcservice.pb.h> | |
| #include <shadercached/proto_bindings/shadercached.pb.h> | |
| #include <vm_protos/proto_bindings/container_host.pb.h> | |
| #include <vm_protos/proto_bindings/vm_host.pb.h> | |
| namespace vm_tools::cicerone { | |
| namespace { | |
| void ShaderCacheMountStatusChanged( | |
| std::string* error_out, | |
| base::WaitableEvent* event, | |
| bool expected_mount, | |
| const shadercached::ShaderCacheMountStatus& mount_status, | |
| bool was_replaced) { | |
| if (was_replaced) { | |
| *error_out = "Another garcon call overrode the waiting request"; | |
| } else if (!mount_status.error().empty()) { | |
| *error_out = mount_status.error(); | |
| } else if (mount_status.mounted() == expected_mount) { | |
| LOG(INFO) << "Shader cache successfully " | |
| << (expected_mount ? "mounted" : "unmounted"); | |
| *error_out = ""; | |
| } else { | |
| // |mounted| does not equate to |expected_mount| despite having no error | |
| LOG(WARNING) << "Unexpected mount status mismatch for " | |
| << mount_status.vm_name(); | |
| *error_out = | |
| base::StringPrintf("Unexpected mount status, expected: %d, got %d", | |
| expected_mount, mount_status.mounted()); | |
| } | |
| event->Signal(); | |
| } | |
| } // namespace | |
| ShadercachedHelper::ShadercachedHelper(dbus::ObjectProxy* shadercached_proxy) { | |
| connected_ = false; | |
| shadercached_proxy->ConnectToSignal( | |
| shadercached::kShaderCacheInterface, | |
| shadercached::kShaderCacheMountStatusChanged, | |
| base::BindRepeating(&ShadercachedHelper::MountStatusChanged, | |
| weak_ptr_factory_.GetWeakPtr()), | |
| base::BindOnce(&ShadercachedHelper::ConnectedToShadercached, | |
| weak_ptr_factory_.GetWeakPtr())); | |
| } | |
| void ShadercachedHelper::ConnectedToShadercached(const std::string& interface, | |
| const std::string& signal, | |
| bool success) { | |
| connected_ = success; | |
| LOG_IF(ERROR, !success) | |
| << "Failed to create ShadercachedHelper, connection to signal failed"; | |
| } | |
| void ShadercachedHelper::InstallShaderCache( | |
| const std::string& owner_id, | |
| const std::string& vm_name, | |
| const vm_tools::container::InstallShaderCacheRequest* request, | |
| std::string* error_out, | |
| base::WaitableEvent* event, | |
| dbus::ObjectProxy* shadercached_proxy) { | |
| LOG(INFO) << "InstallShaderCache called"; | |
| if (!connected_) { | |
| *error_out = "Not connected to shadercached signals"; | |
| event->Signal(); | |
| return; | |
| } | |
| ShadercachedHelper::CallbackCondition condition{ | |
| .vm_name = vm_name, | |
| .owner_id = owner_id, | |
| .steam_app_id = request->steam_app_id()}; | |
| bool callback_added = false; | |
| if (request->wait()) { | |
| // Only add call back if request is waiting for completion | |
| callback_added = | |
| AddCallback(condition, /*expected_mount=*/true, error_out, event); | |
| if (!callback_added) { | |
| LOG(WARNING) << "Failed to add callback for install, still attempting " | |
| "install/mount"; | |
| } | |
| } | |
| dbus::MethodCall method_call(shadercached::kShaderCacheInterface, | |
| shadercached::kInstallMethod); | |
| dbus::MessageWriter shadercached_writer(&method_call); | |
| shadercached::InstallRequest shader_request; | |
| shader_request.set_mount(request->mount()); | |
| shader_request.set_steam_app_id(request->steam_app_id()); | |
| shader_request.set_vm_name(vm_name); | |
| shader_request.set_vm_owner_id(owner_id); | |
| shadercached_writer.AppendProtoAsArrayOfBytes(shader_request); | |
| base::expected<std::unique_ptr<dbus::Response>, dbus::Error> dbus_response = | |
| shadercached_proxy->CallMethodAndBlock( | |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT); | |
| if (!dbus_response.has_value()) { | |
| dbus::Error error = std::move(dbus_response.error()); | |
| *error_out = | |
| base::StringPrintf("%s %s: %s", shadercached::kShaderCacheInterface, | |
| error.name().c_str(), error.message().c_str()); | |
| if (request->wait()) { | |
| mount_callbacks_.erase(condition); | |
| } | |
| event->Signal(); | |
| return; | |
| } | |
| if (!dbus_response.value()) { | |
| *error_out = "Failed to get non-null response."; | |
| if (request->wait()) { | |
| mount_callbacks_.erase(condition); | |
| } | |
| event->Signal(); | |
| return; | |
| } | |
| if (!request->wait()) { | |
| // Signal if we don't have to wait, we don't have to worry about Install | |
| // response. | |
| event->Signal(); | |
| return; | |
| } | |
| shadercached::InstallResponse response; | |
| auto reader = dbus::MessageReader(dbus_response.value().get()); | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| *error_out = "Failed to parse InstallResponse"; | |
| event->Signal(); | |
| return; | |
| } | |
| if (response.mounted()) { | |
| // If shader cache is already mounted, don't wait for mount signal | |
| if (callback_added) { | |
| mount_callbacks_.erase(condition); | |
| } | |
| event->Signal(); | |
| return; | |
| } | |
| if (!callback_added) { | |
| // If callback wasn't added, signal now. If callback's been added, | |
| // signalling will happen on ShaderCacheMountStatusChanged. | |
| *error_out = | |
| "Unable to notify on mount completion, still queued install/mount"; | |
| event->Signal(); | |
| } | |
| } | |
| void ShadercachedHelper::UninstallShaderCache( | |
| const std::string& owner_id, | |
| const std::string& vm_name, | |
| const vm_tools::container::UninstallShaderCacheRequest* request, | |
| std::string* error_out, | |
| base::WaitableEvent* event, | |
| dbus::ObjectProxy* shadercached_proxy_) { | |
| LOG(INFO) << "UninstallShaderCache called"; | |
| if (!connected_) { | |
| *error_out = "Not connected to shadercached signals"; | |
| event->Signal(); | |
| return; | |
| } | |
| dbus::MethodCall method_call(shadercached::kShaderCacheInterface, | |
| shadercached::kUninstallMethod); | |
| dbus::MessageWriter shadercached_writer(&method_call); | |
| shadercached::UninstallRequest shader_request; | |
| shader_request.set_steam_app_id(request->steam_app_id()); | |
| shadercached_writer.AppendProtoAsArrayOfBytes(shader_request); | |
| base::expected<std::unique_ptr<dbus::Response>, dbus::Error> dbus_response = | |
| shadercached_proxy_->CallMethodAndBlock( | |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT); | |
| if (!dbus_response.has_value()) { | |
| dbus::Error error = std::move(dbus_response.error()); | |
| *error_out = | |
| base::StringPrintf("%s %s: %s", shadercached::kShaderCacheInterface, | |
| error.name().c_str(), error.message().c_str()); | |
| event->Signal(); | |
| return; | |
| } | |
| if (!dbus_response.value()) { | |
| *error_out = "Failed to get non-null response."; | |
| event->Signal(); | |
| return; | |
| } | |
| // We do not need to wait for unmount signal here. Unmount was completed in | |
| // the D-Bus call above and unmount errors would have appeared in D-Bus | |
| // errors. | |
| // This does not wait for DLC uninstallation to complete because game | |
| // uninstalls are independent and orthogonal to DLC uninstalls. | |
| *error_out = ""; | |
| event->Signal(); | |
| } | |
| void ShadercachedHelper::UnmountShaderCache( | |
| const std::string& owner_id, | |
| const std::string& vm_name, | |
| const vm_tools::container::UnmountShaderCacheRequest* request, | |
| std::string* error_out, | |
| base::WaitableEvent* event, | |
| dbus::ObjectProxy* shadercached_proxy) { | |
| LOG(INFO) << "UnmountShaderCache called"; | |
| if (!connected_) { | |
| *error_out = "Not connected to shadercached signals"; | |
| event->Signal(); | |
| return; | |
| } | |
| ShadercachedHelper::CallbackCondition condition{ | |
| .vm_name = vm_name, | |
| .owner_id = owner_id, | |
| .steam_app_id = request->steam_app_id()}; | |
| if (request->wait() && | |
| !AddCallback(condition, /*expected_mount=*/false, error_out, event)) { | |
| event->Signal(); | |
| return; | |
| } | |
| dbus::MethodCall method_call(shadercached::kShaderCacheInterface, | |
| shadercached::kUnmountMethod); | |
| dbus::MessageWriter shadercached_writer(&method_call); | |
| shadercached::UnmountRequest shader_request; | |
| shader_request.set_steam_app_id(request->steam_app_id()); | |
| shader_request.set_vm_name(vm_name); | |
| shader_request.set_vm_owner_id(owner_id); | |
| shadercached_writer.AppendProtoAsArrayOfBytes(shader_request); | |
| base::expected<std::unique_ptr<dbus::Response>, dbus::Error> dbus_response = | |
| shadercached_proxy->CallMethodAndBlock( | |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT); | |
| if (!dbus_response.has_value()) { | |
| dbus::Error error = std::move(dbus_response.error()); | |
| *error_out = | |
| base::StringPrintf("%s %s: %s", shadercached::kShaderCacheInterface, | |
| error.name().c_str(), error.message().c_str()); | |
| if (request->wait()) { | |
| mount_callbacks_.erase(condition); | |
| } | |
| event->Signal(); | |
| return; | |
| } | |
| if (!dbus_response.value()) { | |
| *error_out = "Failed to get non-null response."; | |
| if (request->wait()) { | |
| mount_callbacks_.erase(condition); | |
| } | |
| event->Signal(); | |
| return; | |
| } | |
| if (!request->wait()) { | |
| // Only signal if we don't have to wait. If wait is set, signal will happen | |
| // at ShaderCacheMountStatusChanged. | |
| *error_out = ""; | |
| event->Signal(); | |
| } | |
| } | |
| bool ShadercachedHelper::AddCallback(const CallbackCondition& condition, | |
| bool expected_mount, | |
| std::string* error_out, | |
| base::WaitableEvent* event_to_notify) { | |
| // If there is already a process waiting for the game to finish downloading, | |
| // send error to the existing process and replace the waiting with the new | |
| // one. | |
| // | |
| // This is to prevent memory increase from misbehaving user (ex. spamming game | |
| // launches) when DLC download is taking time. | |
| // | |
| // On game-launch fossilize, two processes are 'racing' - garcon client that | |
| // waits for shader cache download+mount and on-device foz blob processing. | |
| // If one of them finishes, the other process is killed. | |
| // Game-launch fossilize process may run multiple times in sequence for dx12 | |
| // games. This means if garcon client is waiting but on-device processing | |
| // finishes, garcon client is killed and a new one is created. | |
| // Hence, we have to make the client always wait for the latest garcon call | |
| // for the game. | |
| if (mount_callbacks_.find(condition) != mount_callbacks_.end()) { | |
| LOG(WARNING) << "Already installing shader cache for the Steam app, " | |
| << "replacing the callback"; | |
| shadercached::ShaderCacheMountStatus unused_mount_status; | |
| std::move(mount_callbacks_[condition]).Run(unused_mount_status, true); | |
| } | |
| mount_callbacks_[condition] = | |
| base::BindOnce(&ShaderCacheMountStatusChanged, error_out, event_to_notify, | |
| expected_mount); | |
| return true; | |
| } | |
| void ShadercachedHelper::MountStatusChanged(dbus::Signal* signal) { | |
| shadercached::ShaderCacheMountStatus mount_status; | |
| auto reader = dbus::MessageReader(signal); | |
| if (!reader.PopArrayOfBytesAsProto(&mount_status)) { | |
| LOG(WARNING) << "Failed to parse ShaderCacheMountStatus"; | |
| return; | |
| } | |
| // Generate the key for this signal and find it | |
| CallbackCondition condition{ | |
| .vm_name = mount_status.vm_name(), | |
| .owner_id = mount_status.vm_owner_id(), | |
| .steam_app_id = mount_status.steam_app_id(), | |
| }; | |
| if (mount_callbacks_.find(condition) != mount_callbacks_.end()) { | |
| LOG(INFO) << "Notifying shader cache mount callback for VM " | |
| << mount_status.vm_name(); | |
| std::move(mount_callbacks_[condition]).Run(mount_status, false); | |
| mount_callbacks_.erase(condition); | |
| } | |
| } | |
| } // namespace vm_tools::cicerone |