提交 db8d8a99 编写于 作者: B Brian Osman 提交者: GitHub

Revert "Reland "Run Flutter on iOS and Android with color correct Skia" (#3818)" (#3823)

This reverts commit 2650f529.
上级 2650f529
......@@ -21,7 +21,7 @@ class CompositorContext {
public:
class ScopedFrame {
public:
SkCanvas* canvas() { return canvas_; }
SkCanvas& canvas() { return *canvas_; }
CompositorContext& context() const { return context_; }
......
......@@ -35,7 +35,6 @@ class Layer {
struct PrerollContext {
RasterCache* raster_cache;
GrContext* gr_context;
SkColorSpace* dst_color_space;
SkRect child_paint_bounds;
};
......
......@@ -27,13 +27,11 @@ void LayerTree::Raster(CompositorContext::ScopedFrame& frame,
void LayerTree::Preroll(CompositorContext::ScopedFrame& frame,
bool ignore_raster_cache) {
TRACE_EVENT0("flutter", "LayerTree::Preroll");
SkColorSpace* color_space =
frame.canvas() ? frame.canvas()->imageInfo().colorSpace() : nullptr;
frame.context().raster_cache().SetCheckboardCacheImages(
checkerboard_raster_cache_images_);
Layer::PrerollContext context = {
ignore_raster_cache ? nullptr : &frame.context().raster_cache(),
frame.gr_context(), color_space, SkRect::MakeEmpty(),
frame.gr_context(), SkRect::MakeEmpty(),
};
root_layer_->Preroll(&context, SkMatrix::I());
}
......@@ -53,7 +51,7 @@ void LayerTree::UpdateScene(SceneUpdateContext& context,
#endif
void LayerTree::Paint(CompositorContext::ScopedFrame& frame) {
Layer::PaintContext context = {*frame.canvas(), frame.context().frame_time(),
Layer::PaintContext context = {frame.canvas(), frame.context().frame_time(),
frame.context().engine_time(),
frame.context().memory_usage(),
checkerboard_offscreen_layers_};
......
......@@ -23,8 +23,7 @@ PictureLayer::~PictureLayer() {
void PictureLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) {
if (auto cache = context->raster_cache) {
raster_cache_result_ = cache->GetPrerolledImage(
context->gr_context, picture_.get(), matrix, context->dst_color_space,
is_complex_, will_change_);
context->gr_context, picture_.get(), matrix, is_complex_, will_change_);
}
SkRect bounds = picture_->cullRect().makeOffset(offset_.x(), offset_.y());
......
......@@ -71,7 +71,6 @@ static bool IsPictureWorthRasterizing(SkPicture* picture,
RasterCacheResult RasterizePicture(SkPicture* picture,
GrContext* context,
const MatrixDecomposition& matrix,
SkColorSpace* dst_color_space,
bool checkerboard) {
TRACE_EVENT0("flutter", "RasterCachePopulate");
......@@ -82,7 +81,7 @@ RasterCacheResult RasterizePicture(SkPicture* picture,
std::ceil(logical_rect.width() * std::abs(scale.x())), // physical width
std::ceil(logical_rect.height() *
std::abs(scale.y())), // physical height
sk_ref_sp(dst_color_space) // colorspace
nullptr // colorspace
);
sk_sp<SkSurface> surface =
......@@ -131,7 +130,6 @@ RasterCacheResult RasterCache::GetPrerolledImage(
GrContext* context,
SkPicture* picture,
const SkMatrix& transformation_matrix,
SkColorSpace* dst_color_space,
bool is_complex,
bool will_change) {
if (!IsPictureWorthRasterizing(picture, will_change, is_complex)) {
......@@ -161,8 +159,7 @@ RasterCacheResult RasterCache::GetPrerolledImage(
if (!entry.image.is_valid()) {
entry.image =
RasterizePicture(picture, context, matrix, dst_color_space,
checkerboard_images_);
RasterizePicture(picture, context, matrix, checkerboard_images_);
}
// We are not considering unrasterizable images. So if we don't have an image
......
......@@ -53,10 +53,8 @@ class RasterCache {
RasterCacheResult GetPrerolledImage(GrContext* context,
SkPicture* picture,
const SkMatrix& transformation_matrix,
SkColorSpace* dst_color_space,
bool is_complex,
bool will_change);
void SweepAfterFrame();
void Clear();
......
......@@ -32,15 +32,14 @@ TEST(RasterCache, ThresholdIsRespected) {
sk_sp<SkImage> image;
sk_sp<SkColorSpace> srgb = SkColorSpace::MakeSRGB();
ASSERT_FALSE(
cache.GetPrerolledImage(NULL, picture.get(), matrix, srgb, true, false)); // 1
cache.GetPrerolledImage(NULL, picture.get(), matrix, true, false)); // 1
cache.SweepAfterFrame();
ASSERT_FALSE(
cache.GetPrerolledImage(NULL, picture.get(), matrix, srgb, true, false)); // 2
cache.GetPrerolledImage(NULL, picture.get(), matrix, true, false)); // 2
cache.SweepAfterFrame();
ASSERT_TRUE(
cache.GetPrerolledImage(NULL, picture.get(), matrix, srgb, true, false)); // 3
cache.GetPrerolledImage(NULL, picture.get(), matrix, true, false)); // 3
cache.SweepAfterFrame();
}
......@@ -54,15 +53,14 @@ TEST(RasterCache, ThresholdIsRespectedWhenZero) {
sk_sp<SkImage> image;
sk_sp<SkColorSpace> srgb = SkColorSpace::MakeSRGB();
ASSERT_FALSE(
cache.GetPrerolledImage(NULL, picture.get(), matrix, srgb, true, false)); // 1
cache.GetPrerolledImage(NULL, picture.get(), matrix, true, false)); // 1
cache.SweepAfterFrame();
ASSERT_FALSE(
cache.GetPrerolledImage(NULL, picture.get(), matrix, srgb, true, false)); // 2
cache.GetPrerolledImage(NULL, picture.get(), matrix, true, false)); // 2
cache.SweepAfterFrame();
ASSERT_FALSE(
cache.GetPrerolledImage(NULL, picture.get(), matrix, srgb, true, false)); // 3
cache.GetPrerolledImage(NULL, picture.get(), matrix, true, false)); // 3
cache.SweepAfterFrame();
}
......@@ -76,20 +74,19 @@ TEST(RasterCache, SweepsRemoveUnusedFrames) {
sk_sp<SkImage> image;
sk_sp<SkColorSpace> srgb = SkColorSpace::MakeSRGB();
ASSERT_FALSE(
cache.GetPrerolledImage(NULL, picture.get(), matrix, srgb, true, false)); // 1
cache.GetPrerolledImage(NULL, picture.get(), matrix, true, false)); // 1
cache.SweepAfterFrame();
ASSERT_FALSE(
cache.GetPrerolledImage(NULL, picture.get(), matrix, srgb, true, false)); // 2
cache.GetPrerolledImage(NULL, picture.get(), matrix, true, false)); // 2
cache.SweepAfterFrame();
ASSERT_TRUE(
cache.GetPrerolledImage(NULL, picture.get(), matrix, srgb, true, false)); // 3
cache.GetPrerolledImage(NULL, picture.get(), matrix, true, false)); // 3
cache.SweepAfterFrame();
ASSERT_TRUE(
cache.GetPrerolledImage(NULL, picture.get(), matrix, srgb, true, false)); // 4
cache.GetPrerolledImage(NULL, picture.get(), matrix, true, false)); // 4
cache.SweepAfterFrame();
cache.SweepAfterFrame(); // Extra frame without a preroll image access.
ASSERT_FALSE(
cache.GetPrerolledImage(NULL, picture.get(), matrix, srgb, true, false)); // 5
cache.GetPrerolledImage(NULL, picture.get(), matrix, true, false)); // 5
}
......@@ -32,10 +32,9 @@ sk_sp<SkImage> DecodeImage(sk_sp<SkData> buffer) {
GrContext* context = ResourceContext::Get();
if (context) {
// This acts as a flag to indicate that we want a color space aware decode.
sk_sp<SkColorSpace> dstColorSpace = SkColorSpace::MakeSRGB();
// TODO: Supply actual destination color space once available
return SkImage::MakeCrossContextFromEncoded(context, std::move(buffer),
false, dstColorSpace.get());
false, nullptr);
} else {
return SkImage::MakeFromEncoded(std::move(buffer));
}
......
......@@ -171,7 +171,6 @@ void PlatformView::SetupResourceContextOnIOThreadPerform(
// other threads correctly, so the textures end up blank. For now, suppress
// that feature, which will cause texture uploads to do CPU YUV conversion.
options.fDisableGpuYUVConversion = true;
options.fRequireDecodeDisableForSRGB = false;
blink::ResourceContext::Set(GrContext::Create(
GrBackend::kOpenGL_GrBackend,
......
......@@ -8,7 +8,6 @@
#include "lib/ftl/arraysize.h"
#include "lib/ftl/logging.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "third_party/skia/include/gpu/GrContextOptions.h"
#include "third_party/skia/include/gpu/gl/GrGLInterface.h"
namespace shell {
......@@ -45,12 +44,9 @@ bool GPUSurfaceGL::Setup() {
auto backend_context =
reinterpret_cast<GrBackendContext>(GrGLCreateNativeInterface());
GrContextOptions options;
options.fRequireDecodeDisableForSRGB = false;
context_ =
sk_sp<GrContext>(GrContext::Create(kOpenGL_GrBackend, backend_context,
options));
sk_sp<GrContext>(GrContext::Create(kOpenGL_GrBackend, backend_context));
if (context_ == nullptr) {
FTL_LOG(INFO) << "Failed to setup Skia Gr context.";
return false;
......@@ -108,23 +104,15 @@ bool GPUSurfaceGL::PresentSurface(SkCanvas* canvas) {
}
bool GPUSurfaceGL::SelectPixelConfig(GrPixelConfig* config) {
if (delegate_->ColorSpace() && delegate_->ColorSpace()->gammaCloseToSRGB()) {
FTL_DCHECK(context_->caps()->isConfigRenderable(kSRGBA_8888_GrPixelConfig,
false));
*config = kSRGBA_8888_GrPixelConfig;
return true;
}
// FIXME:
// If sRGB support is not available, we should instead fall back to software.
if (context_->caps()->isConfigRenderable(kRGBA_8888_GrPixelConfig, false)) {
*config = kRGBA_8888_GrPixelConfig;
return true;
}
static const GrPixelConfig kConfigOptions[] = {
kSkia8888_GrPixelConfig, kRGBA_4444_GrPixelConfig,
};
if (context_->caps()->isConfigRenderable(kRGBA_4444_GrPixelConfig, false)) {
*config = kRGBA_4444_GrPixelConfig;
return true;
for (size_t i = 0; i < arraysize(kConfigOptions); i++) {
if (context_->caps()->isConfigRenderable(kConfigOptions[i], false)) {
*config = kConfigOptions[i];
return true;
}
}
return false;
......@@ -136,6 +124,7 @@ sk_sp<SkSurface> GPUSurfaceGL::CreateSurface(const SkISize& size) {
}
GrBackendRenderTargetDesc desc;
if (!SelectPixelConfig(&desc.fConfig)) {
return nullptr;
}
......@@ -146,8 +135,7 @@ sk_sp<SkSurface> GPUSurfaceGL::CreateSurface(const SkISize& size) {
desc.fOrigin = kBottomLeft_GrSurfaceOrigin;
desc.fRenderTargetHandle = delegate_->GLContextFBO();
return SkSurface::MakeFromBackendRenderTarget(context_.get(), desc,
delegate_->ColorSpace(), nullptr);
return SkSurface::MakeFromBackendRenderTarget(context_.get(), desc, nullptr);
}
sk_sp<SkSurface> GPUSurfaceGL::AcquireSurface(const SkISize& size) {
......
......@@ -22,9 +22,6 @@ class GPUSurfaceGLDelegate {
virtual bool GLContextPresent() = 0;
virtual intptr_t GLContextFBO() const = 0;
// TODO: Update Mac desktop and make this pure virtual.
virtual sk_sp<SkColorSpace> ColorSpace() const { return nullptr; }
};
class GPUSurfaceGL : public Surface {
......
......@@ -3,16 +3,8 @@
// found in the LICENSE file.
#include "flutter/shell/platform/android/android_context_gl.h"
#include <EGL/eglext.h>
#include <utility>
#ifndef EGL_GL_COLORSPACE_KHR
#define EGL_GL_COLORSPACE_KHR 0x309D
#endif
#ifndef EGL_GL_COLORSPACE_SRGB_KHR
#define EGL_GL_COLORSPACE_SRGB_KHR 0x3089
#endif
#include <utility>
namespace shell {
......@@ -120,66 +112,43 @@ static bool TeardownSurface(EGLDisplay display, EGLSurface surface) {
}
// For onscreen rendering.
bool AndroidContextGL::CreateWindowSurface(
ftl::RefPtr<AndroidNativeWindow> window) {
static EGLResult<EGLSurface> CreateWindowSurface(
EGLDisplay display,
EGLConfig config,
AndroidNativeWindow::Handle window_handle) {
// The configurations are only required when dealing with extensions or VG.
// We do neither.
window_ = std::move(window);
EGLDisplay display = environment_->Display();
const EGLint srgb_attribs[] = {
EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_SRGB_KHR,
EGL_NONE
};
const EGLint default_attribs[] = {
EGL_NONE
};
const EGLint* attribs = default_attribs;
if (srgb_support_) {
attribs = srgb_attribs;
}
surface_ = eglCreateWindowSurface(
display, config_,
reinterpret_cast<EGLNativeWindowType>(window_->handle()), attribs);
return surface_ != EGL_NO_SURFACE;
EGLSurface surface = eglCreateWindowSurface(
display, config, reinterpret_cast<EGLNativeWindowType>(window_handle),
nullptr);
return {surface != EGL_NO_SURFACE, surface};
}
// For offscreen rendering.
bool AndroidContextGL::CreatePBufferSurface() {
static EGLResult<EGLSurface> CreatePBufferSurface(EGLDisplay display,
EGLConfig config) {
// We only ever create pbuffer surfaces for background resource loading
// contexts. We never bind the pbuffer to anything.
const EGLint attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE};
EGLSurface surface = eglCreatePbufferSurface(display, config, attribs);
return {surface != EGL_NO_SURFACE, surface};
}
EGLDisplay display = environment_->Display();
const EGLint srgb_attribs[] = {
EGL_WIDTH, 1,
EGL_HEIGHT, 1,
EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_SRGB_KHR,
EGL_NONE
};
const EGLint default_attribs[] = {
EGL_WIDTH, 1,
EGL_HEIGHT, 1,
EGL_NONE
};
const EGLint* attribs = default_attribs;
if (srgb_support_) {
attribs = srgb_attribs;
}
surface_ = eglCreatePbufferSurface(display, config_, attribs);
return surface_ != EGL_NO_SURFACE;
static EGLResult<EGLSurface> CreateSurface(
EGLDisplay display,
EGLConfig config,
ftl::RefPtr<AndroidNativeWindow> window) {
return window && window->IsValid()
? CreateWindowSurface(display, config, window->handle())
: CreatePBufferSurface(display, config);
}
AndroidContextGL::AndroidContextGL(ftl::RefPtr<AndroidEnvironmentGL> env,
ftl::RefPtr<AndroidNativeWindow> window,
PlatformView::SurfaceConfig config,
const AndroidContextGL* share_context)
: environment_(env),
window_(nullptr),
window_(window),
config_(nullptr),
surface_(EGL_NO_SURFACE),
context_(EGL_NO_CONTEXT),
......@@ -201,26 +170,25 @@ AndroidContextGL::AndroidContextGL(ftl::RefPtr<AndroidEnvironmentGL> env,
return;
}
// Create a context for the configuration.
// Create a surface for the configuration.
std::tie(success, context_) = CreateContext(
environment_->Display(), config_,
share_context != nullptr ? share_context->context_ : EGL_NO_CONTEXT);
std::tie(success, surface_) =
CreateSurface(environment_->Display(), config_, window_);
if (!success) {
FTL_LOG(ERROR) << "Could not create an EGL context";
FTL_LOG(ERROR) << "Could not create the EGL surface.";
LogLastEGLError();
return;
}
// On its own, this is not enough to guarantee that we will render in
// sRGB mode. We also need to query GL using the GrContext.
// Create a context for the configuration.
const char* exts = eglQueryString(environment_->Display(), EGL_EXTENSIONS);
srgb_support_ = strstr(exts, "EGL_KHR_gl_colorspace");
std::tie(success, context_) = CreateContext(
environment_->Display(), config_,
share_context != nullptr ? share_context->context_ : EGL_NO_CONTEXT);
if (!this->CreatePBufferSurface()) {
FTL_LOG(ERROR) << "Could not create the EGL surface.";
if (!success) {
FTL_LOG(ERROR) << "Could not create an EGL context";
LogLastEGLError();
return;
}
......@@ -229,6 +197,11 @@ AndroidContextGL::AndroidContextGL(ftl::RefPtr<AndroidEnvironmentGL> env,
valid_ = true;
}
AndroidContextGL::AndroidContextGL(ftl::RefPtr<AndroidEnvironmentGL> env,
PlatformView::SurfaceConfig config,
const AndroidContextGL* share_context)
: AndroidContextGL(env, nullptr, config, share_context) {}
AndroidContextGL::~AndroidContextGL() {
if (!TeardownContext(environment_->Display(), context_)) {
FTL_LOG(ERROR) << "Could not tear down the EGL context. Possible resource leak.";
......@@ -297,18 +270,18 @@ bool AndroidContextGL::Resize(const SkISize& size) {
TeardownSurface(environment_->Display(), surface_);
if (!this->CreateWindowSurface(window_)) {
bool success = false;
std::tie(success, surface_) =
CreateSurface(environment_->Display(), config_, window_);
MakeCurrent();
if (!success) {
FTL_LOG(ERROR) << "Unable to create EGL window surface on resize.";
return false;
}
MakeCurrent();
return true;
}
bool AndroidContextGL::SupportsSRGB() const {
return srgb_support_;
}
} // namespace shell
......@@ -17,11 +17,6 @@ namespace shell {
class AndroidContextGL : public ftl::RefCountedThreadSafe<AndroidContextGL> {
public:
bool CreateWindowSurface(ftl::RefPtr<AndroidNativeWindow> window);
bool CreatePBufferSurface();
ftl::RefPtr<AndroidEnvironmentGL> Environment() const;
bool IsValid() const;
......@@ -36,17 +31,22 @@ class AndroidContextGL : public ftl::RefCountedThreadSafe<AndroidContextGL> {
bool Resize(const SkISize& size);
bool SupportsSRGB() const;
private:
ftl::RefPtr<AndroidEnvironmentGL> environment_;
ftl::RefPtr<AndroidNativeWindow> window_;
EGLConfig config_;
EGLSurface surface_;
EGLContext context_;
bool srgb_support_;
bool valid_;
/// Creates a window surface context tied to the window handle for on-screen
/// rendering.
AndroidContextGL(ftl::RefPtr<AndroidEnvironmentGL> env,
ftl::RefPtr<AndroidNativeWindow> window,
PlatformView::SurfaceConfig config,
const AndroidContextGL* share_context = nullptr);
/// Creates a pbuffer surface context for offscreen rendering.
AndroidContextGL(ftl::RefPtr<AndroidEnvironmentGL> env,
PlatformView::SurfaceConfig config,
const AndroidContextGL* share_context = nullptr);
......
......@@ -117,7 +117,7 @@ bool AndroidSurfaceGL::SetNativeWindow(ftl::RefPtr<AndroidNativeWindow> window,
// Create the onscreen context.
onscreen_context_ = ftl::MakeRefCounted<AndroidContextGL>(
offscreen_context_->Environment(), config,
offscreen_context_->Environment(), std::move(window), config,
offscreen_context_.get() /* sharegroup */);
if (!onscreen_context_->IsValid()) {
......@@ -125,11 +125,6 @@ bool AndroidSurfaceGL::SetNativeWindow(ftl::RefPtr<AndroidNativeWindow> window,
return false;
}
if (!onscreen_context_->CreateWindowSurface(std::move(window))) {
onscreen_context_ = nullptr;
return false;
}
return true;
}
......
......@@ -47,21 +47,12 @@ class AndroidSurfaceGL : public GPUSurfaceGLDelegate, public AndroidSurface {
intptr_t GLContextFBO() const override;
sk_sp<SkColorSpace> ColorSpace() const override {
// TODO:
// We can render more consistently across devices when Android makes it
// possible to query for the color space of the display.
return onscreen_context_->SupportsSRGB() ? SkColorSpace::MakeSRGB()
: nullptr;
}
void SetFlutterView(
const fml::jni::JavaObjectWeakGlobalRef& flutter_view) override;
private:
ftl::RefPtr<AndroidContextGL> onscreen_context_;
ftl::RefPtr<AndroidContextGL> offscreen_context_;
sk_sp<GrContext> gr_context_;
FTL_DISALLOW_COPY_AND_ASSIGN(AndroidSurfaceGL);
};
......
......@@ -34,10 +34,6 @@ class IOSGLContext {
bool ResourceMakeCurrent();
sk_sp<SkColorSpace> ColorSpace() const {
return color_space_;
}
private:
fml::scoped_nsobject<CAEAGLLayer> layer_;
fml::scoped_nsobject<EAGLContext> context_;
......@@ -49,7 +45,6 @@ class IOSGLContext {
GLuint depth_stencil_packed_buffer_;
GLint storage_size_width_;
GLint storage_size_height_;
sk_sp<SkColorSpace> color_space_;
bool valid_;
FTL_DISALLOW_COPY_AND_ASSIGN(IOSGLContext);
......
......@@ -3,10 +3,6 @@
// found in the LICENSE file.
#include "flutter/shell/platform/darwin/ios/ios_gl_context.h"
#include "third_party/skia/include/gpu/GrContextOptions.h"
#include "third_party/skia/include/gpu/gl/GrGLInterface.h"
#include <UIKit/UIKit.h>
namespace shell {
......@@ -100,32 +96,14 @@ IOSGLContext::IOSGLContext(PlatformView::SurfaceConfig config, CAEAGLLayer* laye
}
}
// TODO:
// iOS displays are more variable than just P3 or sRGB. Reading the display
// gamut just tells us what color space it makes sense to render into. We
// should use iOS APIs to perform the final correction step based on the
// device properties. Ex: We can indicate that we have rendered in P3, and
// the framework will do the final adjustment for us.
NSOperatingSystemVersion version = [[NSProcessInfo processInfo]
operatingSystemVersion];
color_space_ = SkColorSpace::MakeSRGB();
if (version.majorVersion >= 10) {
UIDisplayGamut displayGamut =
[UIScreen mainScreen].traitCollection.displayGamut;
switch (displayGamut) {
case UIDisplayGamutP3:
// Should we consider using more than 8-bits of precision given that
// P3 specifies a wider range of colors?
color_space_ = SkColorSpace::MakeRGB(
SkColorSpace::kSRGB_RenderTargetGamma,
SkColorSpace::kDCIP3_D65_Gamut);
break;
default:
break;
}
// The default is RGBA
NSString* drawableColorFormat = kEAGLColorFormatRGBA8;
if (config.red_bits <= 5 && config.green_bits <= 6 && config.blue_bits <= 5 &&
config.alpha_bits == 0) {
drawableColorFormat = kEAGLColorFormatRGB565;
}
NSString* drawableColorFormat = kEAGLColorFormatSRGBA8;
layer_.get().drawableProperties = @{
kEAGLDrawablePropertyColorFormat : drawableColorFormat,
kEAGLDrawablePropertyRetainedBacking : @(NO),
......
......@@ -36,10 +36,6 @@ class IOSSurfaceGL : public IOSSurface, public GPUSurfaceGLDelegate {
intptr_t GLContextFBO() const override;
sk_sp<SkColorSpace> ColorSpace() const override {
return context_.ColorSpace();
}
private:
IOSGLContext context_;
......
......@@ -63,9 +63,8 @@ sk_sp<SkSurface> IOSSurfaceSoftware::AcquireBackingStore(const SkISize& size) {
return sk_surface_;
}
SkImageInfo info = SkImageInfo::MakeS32(size.fWidth, size.fHeight,
kPremul_SkAlphaType);
sk_surface_ = SkSurface::MakeRaster(info, nullptr);
sk_surface_ = SkSurface::MakeRasterN32Premul(size.fWidth, size.fHeight,
nullptr /* SkSurfaceProps as out */);
return sk_surface_;
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册