| OLD | NEW |
| 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "content/common/gpu/client/context_provider_command_buffer.h" | 5 #include "content/common/gpu/client/context_provider_command_buffer.h" |
| 6 | 6 |
| 7 #include <stddef.h> | 7 #include <stddef.h> |
| 8 | 8 |
| 9 #include <memory> | 9 #include <memory> |
| 10 #include <set> | 10 #include <set> |
| 11 #include <utility> | 11 #include <utility> |
| 12 #include <vector> | 12 #include <vector> |
| 13 | 13 |
| 14 #include "base/callback_helpers.h" | 14 #include "base/callback_helpers.h" |
| 15 #include "base/command_line.h" | 15 #include "base/command_line.h" |
| 16 #include "base/strings/stringprintf.h" | 16 #include "base/strings/stringprintf.h" |
| 17 #include "cc/output/managed_memory_policy.h" | 17 #include "cc/output/managed_memory_policy.h" |
| 18 #include "content/common/gpu/client/command_buffer_metrics.h" | 18 #include "content/common/gpu/client/command_buffer_metrics.h" |
| 19 #include "content/common/gpu/client/webgraphicscontext3d_command_buffer_impl.h" | 19 #include "gpu/command_buffer/client/gles2_cmd_helper.h" |
| 20 #include "gpu/command_buffer/client/gles2_implementation.h" | 20 #include "gpu/command_buffer/client/gles2_implementation.h" |
| 21 #include "gpu/command_buffer/client/gles2_trace_implementation.h" | 21 #include "gpu/command_buffer/client/gles2_trace_implementation.h" |
| 22 #include "gpu/command_buffer/client/gpu_switches.h" | 22 #include "gpu/command_buffer/client/gpu_switches.h" |
| 23 #include "gpu/command_buffer/client/transfer_buffer.h" |
| 24 #include "gpu/command_buffer/common/constants.h" |
| 25 #include "gpu/ipc/client/command_buffer_proxy_impl.h" |
| 23 #include "gpu/ipc/client/gpu_channel_host.h" | 26 #include "gpu/ipc/client/gpu_channel_host.h" |
| 24 #include "gpu/skia_bindings/grcontext_for_gles2_interface.h" | 27 #include "gpu/skia_bindings/grcontext_for_gles2_interface.h" |
| 25 #include "third_party/skia/include/gpu/GrContext.h" | 28 #include "third_party/skia/include/gpu/GrContext.h" |
| 26 | 29 |
| 30 namespace { |
| 31 |
| 32 // Similar to base::AutoReset but it sets the variable to the new value |
| 33 // when it is destroyed. Use reset() to cancel setting the variable. |
| 34 class AutoSet { |
| 35 public: |
| 36 AutoSet(bool* b, bool set) : b_(b), set_(set) {} |
| 37 ~AutoSet() { |
| 38 if (b_) |
| 39 *b_ = set_; |
| 40 } |
| 41 // Stops us from setting _b on destruction. |
| 42 void Reset() { b_ = nullptr; } |
| 43 |
| 44 private: |
| 45 bool* b_; |
| 46 const bool set_; |
| 47 }; |
| 48 } |
| 49 |
| 27 namespace content { | 50 namespace content { |
| 28 | 51 |
| 29 ContextProviderCommandBuffer::SharedProviders::SharedProviders() = default; | 52 ContextProviderCommandBuffer::SharedProviders::SharedProviders() = default; |
| 30 ContextProviderCommandBuffer::SharedProviders::~SharedProviders() = default; | 53 ContextProviderCommandBuffer::SharedProviders::~SharedProviders() = default; |
| 31 | 54 |
| 32 ContextProviderCommandBuffer::ContextProviderCommandBuffer( | 55 ContextProviderCommandBuffer::ContextProviderCommandBuffer( |
| 33 scoped_refptr<gpu::GpuChannelHost> channel, | 56 scoped_refptr<gpu::GpuChannelHost> channel, |
| 34 gpu::SurfaceHandle surface_handle, | 57 gpu::SurfaceHandle surface_handle, |
| 35 const GURL& active_url, | 58 const GURL& active_url, |
| 36 gfx::GpuPreference gpu_preference, | 59 gfx::GpuPreference gpu_preference, |
| 37 bool automatic_flushes, | 60 bool automatic_flushes, |
| 38 const gpu::SharedMemoryLimits& memory_limits, | 61 const gpu::SharedMemoryLimits& memory_limits, |
| 39 const gpu::gles2::ContextCreationAttribHelper& attributes, | 62 const gpu::gles2::ContextCreationAttribHelper& attributes, |
| 40 ContextProviderCommandBuffer* shared_context_provider, | 63 ContextProviderCommandBuffer* shared_context_provider, |
| 41 command_buffer_metrics::ContextType type) | 64 command_buffer_metrics::ContextType type) |
| 42 : surface_handle_(surface_handle), | 65 : surface_handle_(surface_handle), |
| 43 active_url_(active_url), | 66 active_url_(active_url), |
| 44 gpu_preference_(gpu_preference), | 67 gpu_preference_(gpu_preference), |
| 45 automatic_flushes_(automatic_flushes), | 68 automatic_flushes_(automatic_flushes), |
| 46 memory_limits_(memory_limits), | 69 memory_limits_(memory_limits), |
| 47 attributes_(attributes), | 70 attributes_(attributes), |
| 48 context_type_(type), | 71 context_type_(type), |
| 49 shared_providers_(shared_context_provider | 72 shared_providers_(shared_context_provider |
| 50 ? shared_context_provider->shared_providers_ | 73 ? shared_context_provider->shared_providers_ |
| 51 : new SharedProviders), | 74 : new SharedProviders), |
| 52 channel_(std::move(channel)), | 75 channel_(std::move(channel)) { |
| 53 context3d_(new WebGraphicsContext3DCommandBufferImpl) { | |
| 54 DCHECK(main_thread_checker_.CalledOnValidThread()); | 76 DCHECK(main_thread_checker_.CalledOnValidThread()); |
| 55 DCHECK(channel_); | 77 DCHECK(channel_); |
| 56 context_thread_checker_.DetachFromThread(); | 78 context_thread_checker_.DetachFromThread(); |
| 57 } | 79 } |
| 58 | 80 |
| 59 ContextProviderCommandBuffer::~ContextProviderCommandBuffer() { | 81 ContextProviderCommandBuffer::~ContextProviderCommandBuffer() { |
| 60 DCHECK(main_thread_checker_.CalledOnValidThread() || | 82 DCHECK(main_thread_checker_.CalledOnValidThread() || |
| 61 context_thread_checker_.CalledOnValidThread()); | 83 context_thread_checker_.CalledOnValidThread()); |
| 62 | 84 |
| 63 { | 85 { |
| 64 base::AutoLock hold(shared_providers_->lock); | 86 base::AutoLock hold(shared_providers_->lock); |
| 65 auto it = std::find(shared_providers_->list.begin(), | 87 auto it = std::find(shared_providers_->list.begin(), |
| 66 shared_providers_->list.end(), this); | 88 shared_providers_->list.end(), this); |
| 67 if (it != shared_providers_->list.end()) | 89 if (it != shared_providers_->list.end()) |
| 68 shared_providers_->list.erase(it); | 90 shared_providers_->list.erase(it); |
| 69 } | 91 } |
| 70 | 92 |
| 71 if (bind_succeeded_) { | 93 if (bind_succeeded_) { |
| 72 // Clear the lock to avoid DCHECKs that the lock is being held during | 94 // Clear the lock to avoid DCHECKs that the lock is being held during |
| 73 // shutdown. | 95 // shutdown. |
| 74 context3d_->GetCommandBufferProxy()->SetLock(nullptr); | 96 command_buffer_->SetLock(nullptr); |
| 75 // Disconnect lost callbacks during destruction. | 97 // Disconnect lost callbacks during destruction. |
| 76 context3d_->GetImplementation()->SetLostContextCallback(base::Closure()); | 98 gles2_impl_->SetLostContextCallback(base::Closure()); |
| 77 } | 99 } |
| 78 } | 100 } |
| 79 | 101 |
| 80 gpu::CommandBufferProxyImpl* | 102 gpu::CommandBufferProxyImpl* |
| 81 ContextProviderCommandBuffer::GetCommandBufferProxy() { | 103 ContextProviderCommandBuffer::GetCommandBufferProxy() { |
| 82 return context3d_->GetCommandBufferProxy(); | 104 return command_buffer_.get(); |
| 83 } | 105 } |
| 84 | 106 |
| 85 bool ContextProviderCommandBuffer::BindToCurrentThread() { | 107 bool ContextProviderCommandBuffer::BindToCurrentThread() { |
| 86 // This is called on the thread the context will be used. | 108 // This is called on the thread the context will be used. |
| 87 DCHECK(context_thread_checker_.CalledOnValidThread()); | 109 DCHECK(context_thread_checker_.CalledOnValidThread()); |
| 88 | 110 |
| 89 if (!context3d_) | 111 if (bind_failed_) |
| 90 return false; // Already failed. | 112 return false; |
| 91 if (bind_succeeded_) | 113 if (bind_succeeded_) |
| 92 return true; // Already succeeded. | 114 return true; |
| 115 |
| 116 // Early outs should report failure. |
| 117 AutoSet set_bind_failed(&bind_failed_, true); |
| 93 | 118 |
| 94 // It's possible to be running BindToCurrentThread on two contexts | 119 // It's possible to be running BindToCurrentThread on two contexts |
| 95 // on different threads at the same time, but which will be in the same share | 120 // on different threads at the same time, but which will be in the same share |
| 96 // group. To ensure they end up in the same group, hold the lock on the | 121 // group. To ensure they end up in the same group, hold the lock on the |
| 97 // shared_providers_ (which they will share) after querying the group, until | 122 // shared_providers_ (which they will share) after querying the group, until |
| 98 // this context has been added to the list. | 123 // this context has been added to the list. |
| 99 { | 124 { |
| 100 ContextProviderCommandBuffer* shared_context_provider = nullptr; | 125 ContextProviderCommandBuffer* shared_context_provider = nullptr; |
| 101 gpu::CommandBufferProxyImpl* shared_command_buffer = nullptr; | 126 gpu::CommandBufferProxyImpl* shared_command_buffer = nullptr; |
| 102 scoped_refptr<gpu::gles2::ShareGroup> share_group; | 127 scoped_refptr<gpu::gles2::ShareGroup> share_group; |
| 103 | 128 |
| 104 base::AutoLock hold(shared_providers_->lock); | 129 base::AutoLock hold(shared_providers_->lock); |
| 105 | 130 |
| 106 if (!shared_providers_->list.empty()) { | 131 if (!shared_providers_->list.empty()) { |
| 107 shared_context_provider = shared_providers_->list.front(); | 132 shared_context_provider = shared_providers_->list.front(); |
| 108 shared_command_buffer = | 133 shared_command_buffer = shared_context_provider->command_buffer_.get(); |
| 109 shared_context_provider->context3d_->GetCommandBufferProxy(); | 134 share_group = shared_context_provider->gles2_impl_->share_group(); |
| 110 share_group = shared_context_provider->context3d_->GetImplementation() | 135 DCHECK_EQ(!!shared_command_buffer, !!share_group); |
| 111 ->share_group(); | |
| 112 } | 136 } |
| 113 | 137 |
| 114 if (!context3d_->InitializeOnCurrentThread( | 138 DCHECK(attributes_.buffer_preserved); |
| 115 surface_handle_, active_url_, channel_.get(), gpu_preference_, | 139 std::vector<int32_t> serialized_attributes; |
| 116 automatic_flushes_, memory_limits_, shared_command_buffer, | 140 attributes_.Serialize(&serialized_attributes); |
| 117 share_group, attributes_, context_type_)) { | 141 |
| 118 context3d_ = nullptr; | 142 // This command buffer is a client-side proxy to the command buffer in the |
| 143 // GPU process. |
| 144 command_buffer_ = channel_->CreateCommandBuffer( |
| 145 surface_handle_, gfx::Size(), shared_command_buffer, |
| 146 gpu::GpuChannelHost::kDefaultStreamId, |
| 147 gpu::GpuChannelHost::kDefaultStreamPriority, serialized_attributes, |
| 148 active_url_, gpu_preference_); |
| 149 // The command buffer takes ownership of the |channel_|, so no need to keep |
| 150 // a reference around here. |
| 151 channel_ = nullptr; |
| 152 if (!command_buffer_) { |
| 153 DLOG(ERROR) << "GpuChannelHost failed to create command buffer."; |
| 154 command_buffer_metrics::UmaRecordContextInitFailed(context_type_); |
| 119 return false; | 155 return false; |
| 120 } | 156 } |
| 121 | 157 |
| 158 // The GLES2 helper writes the command buffer protocol. |
| 159 gles2_helper_.reset(new gpu::gles2::GLES2CmdHelper(command_buffer_.get())); |
| 160 gles2_helper_->SetAutomaticFlushes(automatic_flushes_); |
| 161 if (!gles2_helper_->Initialize(memory_limits_.command_buffer_size)) { |
| 162 DLOG(ERROR) << "Failed to initialize GLES2CmdHelper."; |
| 163 return false; |
| 164 } |
| 165 |
| 166 // The transfer buffer is used to copy resources between the client |
| 167 // process and the GPU process. |
| 168 transfer_buffer_.reset(new gpu::TransferBuffer(gles2_helper_.get())); |
| 169 |
| 170 // The GLES2Implementation exposes the OpenGLES2 API, as well as the |
| 171 // gpu::ContextSupport interface. |
| 172 constexpr bool support_client_side_arrays = false; |
| 173 gles2_impl_.reset(new gpu::gles2::GLES2Implementation( |
| 174 gles2_helper_.get(), share_group, transfer_buffer_.get(), |
| 175 attributes_.bind_generates_resource, |
| 176 attributes_.lose_context_when_out_of_memory, support_client_side_arrays, |
| 177 command_buffer_.get())); |
| 178 if (!gles2_impl_->Initialize(memory_limits_.start_transfer_buffer_size, |
| 179 memory_limits_.min_transfer_buffer_size, |
| 180 memory_limits_.max_transfer_buffer_size, |
| 181 memory_limits_.mapped_memory_reclaim_limit)) { |
| 182 DLOG(ERROR) << "Failed to initialize GLES2Implementation."; |
| 183 return false; |
| 184 } |
| 185 |
| 186 if (command_buffer_->GetLastError() != gpu::error::kNoError) { |
| 187 DLOG(ERROR) << "Context dead on arrival. Last error: " |
| 188 << command_buffer_->GetLastError(); |
| 189 return false; |
| 190 } |
| 191 |
| 122 // If any context in the share group has been lost, then abort and don't | 192 // If any context in the share group has been lost, then abort and don't |
| 123 // continue since we need to go back to the caller of the constructor to | 193 // continue since we need to go back to the caller of the constructor to |
| 124 // find the correct share group. | 194 // find the correct share group. |
| 125 // This may happen in between the share group being chosen at the | 195 // This may happen in between the share group being chosen at the |
| 126 // constructor, and getting to run this BindToCurrentThread method which | 196 // constructor, and getting to run this BindToCurrentThread method which |
| 127 // can be on some other thread. | 197 // can be on some other thread. |
| 128 // We intentionally call this *after* creating the command buffer via the | 198 // We intentionally call this *after* creating the command buffer via the |
| 129 // GpuChannelHost. Once that has happened, the service knows we are in the | 199 // GpuChannelHost. Once that has happened, the service knows we are in the |
| 130 // share group and if a shared context is lost, our context will be informed | 200 // share group and if a shared context is lost, our context will be informed |
| 131 // also, and the lost context callback will occur for the owner of the | 201 // also, and the lost context callback will occur for the owner of the |
| 132 // context provider. If we check sooner, the shared context may be lost in | 202 // context provider. If we check sooner, the shared context may be lost in |
| 133 // between these two states and our context here would be left in an orphan | 203 // between these two states and our context here would be left in an orphan |
| 134 // share group. | 204 // share group. |
| 135 if (share_group && share_group->IsLost()) { | 205 if (share_group && share_group->IsLost()) |
| 136 context3d_ = nullptr; | |
| 137 return false; | 206 return false; |
| 138 } | |
| 139 | 207 |
| 140 shared_providers_->list.push_back(this); | 208 shared_providers_->list.push_back(this); |
| 141 } | 209 } |
| 210 set_bind_failed.Reset(); |
| 211 bind_succeeded_ = true; |
| 142 | 212 |
| 143 context3d_->GetImplementation()->SetLostContextCallback( | 213 gles2_impl_->SetLostContextCallback( |
| 144 base::Bind(&ContextProviderCommandBuffer::OnLostContext, | 214 base::Bind(&ContextProviderCommandBuffer::OnLostContext, |
| 145 // |this| owns the GLES2Implementation which holds the | 215 // |this| owns the GLES2Implementation which holds the |
| 146 // callback. | 216 // callback. |
| 147 base::Unretained(this))); | 217 base::Unretained(this))); |
| 148 | 218 |
| 149 if (base::CommandLine::ForCurrentProcess()->HasSwitch( | 219 if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| 150 switches::kEnableGpuClientTracing)) { | 220 switches::kEnableGpuClientTracing)) { |
| 151 // This wraps the real GLES2Implementation and we should always use this | 221 // This wraps the real GLES2Implementation and we should always use this |
| 152 // instead when it's present. | 222 // instead when it's present. |
| 153 trace_impl_.reset(new gpu::gles2::GLES2TraceImplementation( | 223 trace_impl_.reset( |
| 154 context3d_->GetImplementation())); | 224 new gpu::gles2::GLES2TraceImplementation(gles2_impl_.get())); |
| 155 } | 225 } |
| 156 | 226 |
| 157 bind_succeeded_ = true; | |
| 158 | |
| 159 // Do this last once the context is set up. | 227 // Do this last once the context is set up. |
| 160 std::string type_name = | 228 std::string type_name = |
| 161 command_buffer_metrics::ContextTypeToString(context_type_); | 229 command_buffer_metrics::ContextTypeToString(context_type_); |
| 162 std::string unique_context_name = | 230 std::string unique_context_name = |
| 163 base::StringPrintf("%s-%p", type_name.c_str(), context3d_.get()); | 231 base::StringPrintf("%s-%p", type_name.c_str(), gles2_impl_.get()); |
| 164 ContextGL()->TraceBeginCHROMIUM("gpu_toplevel", unique_context_name.c_str()); | 232 ContextGL()->TraceBeginCHROMIUM("gpu_toplevel", unique_context_name.c_str()); |
| 165 return true; | 233 return true; |
| 166 } | 234 } |
| 167 | 235 |
| 168 void ContextProviderCommandBuffer::DetachFromThread() { | 236 void ContextProviderCommandBuffer::DetachFromThread() { |
| 169 context_thread_checker_.DetachFromThread(); | 237 context_thread_checker_.DetachFromThread(); |
| 170 } | 238 } |
| 171 | 239 |
| 172 gpu::gles2::GLES2Interface* ContextProviderCommandBuffer::ContextGL() { | 240 gpu::gles2::GLES2Interface* ContextProviderCommandBuffer::ContextGL() { |
| 173 DCHECK(context3d_); | |
| 174 DCHECK(bind_succeeded_); | 241 DCHECK(bind_succeeded_); |
| 175 DCHECK(context_thread_checker_.CalledOnValidThread()); | 242 DCHECK(context_thread_checker_.CalledOnValidThread()); |
| 176 | 243 |
| 177 if (trace_impl_) | 244 if (trace_impl_) |
| 178 return trace_impl_.get(); | 245 return trace_impl_.get(); |
| 179 return context3d_->GetImplementation(); | 246 return gles2_impl_.get(); |
| 180 } | 247 } |
| 181 | 248 |
| 182 gpu::ContextSupport* ContextProviderCommandBuffer::ContextSupport() { | 249 gpu::ContextSupport* ContextProviderCommandBuffer::ContextSupport() { |
| 183 return context3d_->GetImplementation(); | 250 return gles2_impl_.get(); |
| 184 } | 251 } |
| 185 | 252 |
| 186 class GrContext* ContextProviderCommandBuffer::GrContext() { | 253 class GrContext* ContextProviderCommandBuffer::GrContext() { |
| 187 DCHECK(bind_succeeded_); | 254 DCHECK(bind_succeeded_); |
| 188 DCHECK(context_thread_checker_.CalledOnValidThread()); | 255 DCHECK(context_thread_checker_.CalledOnValidThread()); |
| 189 | 256 |
| 190 if (gr_context_) | 257 if (gr_context_) |
| 191 return gr_context_->get(); | 258 return gr_context_->get(); |
| 192 | 259 |
| 193 gr_context_.reset(new skia_bindings::GrContextForGLES2Interface(ContextGL())); | 260 gr_context_.reset(new skia_bindings::GrContextForGLES2Interface(ContextGL())); |
| (...skipping 10 matching lines...) Expand all Loading... |
| 204 if (gr_context_) { | 271 if (gr_context_) { |
| 205 DCHECK(bind_succeeded_); | 272 DCHECK(bind_succeeded_); |
| 206 DCHECK(context_thread_checker_.CalledOnValidThread()); | 273 DCHECK(context_thread_checker_.CalledOnValidThread()); |
| 207 gr_context_->ResetContext(state); | 274 gr_context_->ResetContext(state); |
| 208 } | 275 } |
| 209 } | 276 } |
| 210 | 277 |
| 211 void ContextProviderCommandBuffer::SetupLock() { | 278 void ContextProviderCommandBuffer::SetupLock() { |
| 212 DCHECK(bind_succeeded_); | 279 DCHECK(bind_succeeded_); |
| 213 DCHECK(context_thread_checker_.CalledOnValidThread()); | 280 DCHECK(context_thread_checker_.CalledOnValidThread()); |
| 214 context3d_->GetCommandBufferProxy()->SetLock(&context_lock_); | 281 command_buffer_->SetLock(&context_lock_); |
| 215 } | 282 } |
| 216 | 283 |
| 217 base::Lock* ContextProviderCommandBuffer::GetLock() { | 284 base::Lock* ContextProviderCommandBuffer::GetLock() { |
| 218 return &context_lock_; | 285 return &context_lock_; |
| 219 } | 286 } |
| 220 | 287 |
| 221 gpu::Capabilities ContextProviderCommandBuffer::ContextCapabilities() { | 288 gpu::Capabilities ContextProviderCommandBuffer::ContextCapabilities() { |
| 222 DCHECK(bind_succeeded_); | 289 DCHECK(bind_succeeded_); |
| 223 DCHECK(context_thread_checker_.CalledOnValidThread()); | 290 DCHECK(context_thread_checker_.CalledOnValidThread()); |
| 224 // Skips past the trace_impl_ as it doesn't have capabilities. | 291 // Skips past the trace_impl_ as it doesn't have capabilities. |
| 225 return context3d_->GetImplementation()->capabilities(); | 292 return gles2_impl_->capabilities(); |
| 226 } | 293 } |
| 227 | 294 |
| 228 void ContextProviderCommandBuffer::DeleteCachedResources() { | 295 void ContextProviderCommandBuffer::DeleteCachedResources() { |
| 229 DCHECK(context_thread_checker_.CalledOnValidThread()); | 296 DCHECK(context_thread_checker_.CalledOnValidThread()); |
| 230 | 297 |
| 231 if (gr_context_) | 298 if (gr_context_) |
| 232 gr_context_->FreeGpuResources(); | 299 gr_context_->FreeGpuResources(); |
| 233 } | 300 } |
| 234 | 301 |
| 235 void ContextProviderCommandBuffer::OnLostContext() { | 302 void ContextProviderCommandBuffer::OnLostContext() { |
| (...skipping 11 matching lines...) Expand all Loading... |
| 247 | 314 |
| 248 void ContextProviderCommandBuffer::SetLostContextCallback( | 315 void ContextProviderCommandBuffer::SetLostContextCallback( |
| 249 const LostContextCallback& lost_context_callback) { | 316 const LostContextCallback& lost_context_callback) { |
| 250 DCHECK(context_thread_checker_.CalledOnValidThread()); | 317 DCHECK(context_thread_checker_.CalledOnValidThread()); |
| 251 DCHECK(lost_context_callback_.is_null() || | 318 DCHECK(lost_context_callback_.is_null() || |
| 252 lost_context_callback.is_null()); | 319 lost_context_callback.is_null()); |
| 253 lost_context_callback_ = lost_context_callback; | 320 lost_context_callback_ = lost_context_callback; |
| 254 } | 321 } |
| 255 | 322 |
| 256 } // namespace content | 323 } // namespace content |
| OLD | NEW |