提交 1cb74638 编写于 作者: S serb

8000629: [macosx] Blurry rendering with Java 7 on Retina display

Reviewed-by: anthony, prr, flar
上级 54541a1c
......@@ -219,6 +219,12 @@ public final class CGraphicsDevice extends GraphicsDevice {
return nativeGetDisplayModes(displayID);
}
public int getScaleFactor() {
return (int) nativeGetScaleFactor(displayID);
}
private static native double nativeGetScaleFactor(int displayID);
private static native void nativeSetDisplayMode(int displayID, int w, int h, int bpp, int refrate);
private static native DisplayMode nativeGetDisplayMode(int displayID);
......
......@@ -441,29 +441,37 @@ public final class CGLGraphicsConfig extends CGraphicsConfig
@Override
public int getMaxTextureWidth() {
int width;
//Temporary disable this logic and use some magic constrain.
/*
int width;
synchronized (totalDisplayBounds) {
if (totalDisplayBounds.width == 0) {
updateTotalDisplayBounds();
}
width = totalDisplayBounds.width;
}
synchronized (totalDisplayBounds) {
if (totalDisplayBounds.width == 0) {
updateTotalDisplayBounds();
}
width = totalDisplayBounds.width;
}
return Math.min(width, getMaxTextureSize());
return Math.min(width, getMaxTextureSize());
*/
return getMaxTextureSize() / (getDevice().getScaleFactor() * 2);
}
@Override
public int getMaxTextureHeight() {
int height;
synchronized (totalDisplayBounds) {
if (totalDisplayBounds.height == 0) {
updateTotalDisplayBounds();
}
height = totalDisplayBounds.height;
}
return Math.min(height, getMaxTextureSize());
//Temporary disable this logic and use some magic constrain.
/*
int height;
synchronized (totalDisplayBounds) {
if (totalDisplayBounds.height == 0) {
updateTotalDisplayBounds();
}
height = totalDisplayBounds.height;
}
return Math.min(height, getMaxTextureSize());
*/
return getMaxTextureSize() / (getDevice().getScaleFactor() * 2);
}
}
......@@ -40,11 +40,12 @@ import java.awt.Transparency;
public class CGLLayer extends CFRetainedResource {
private native long nativeCreateLayer();
private static native void nativeSetScale(long layerPtr, double scale);
private static native void validate(long layerPtr, CGLSurfaceData cglsd);
private static native void blitTexture(long layerPtr);
private LWWindowPeer peer;
private int scale = 1;
private SurfaceData surfaceData; // represents intermediate buffer (texture)
......@@ -90,7 +91,7 @@ public class CGLLayer extends CFRetainedResource {
// and blits the buffer to the layer surface (in drawInCGLContext callback)
CGraphicsConfig gc = (CGraphicsConfig)peer.getGraphicsConfiguration();
surfaceData = gc.createSurfaceData(this);
setScale(gc.getDevice().getScaleFactor());
// the layer holds a reference to the buffer, which in
// turn has a reference back to this layer
if (surfaceData instanceof CGLSurfaceData) {
......@@ -121,6 +122,13 @@ public class CGLLayer extends CFRetainedResource {
super.dispose();
}
private void setScale(final int _scale) {
if (scale != _scale) {
scale = _scale;
nativeSetScale(getPointer(), scale);
}
}
// ----------------------------------------------------------------------
// NATIVE CALLBACKS
// ----------------------------------------------------------------------
......
......@@ -30,7 +30,6 @@ import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.image.ColorModel;
......@@ -41,6 +40,9 @@ import sun.lwawt.macosx.CPlatformView;
public abstract class CGLSurfaceData extends OGLSurfaceData {
protected final int scale;
protected final int width;
protected final int height;
protected CPlatformView pView;
private CGLGraphicsConfig graphicsConfig;
......@@ -52,10 +54,19 @@ public abstract class CGLSurfaceData extends OGLSurfaceData {
protected native boolean initPbuffer(long pData, long pConfigInfo,
boolean isOpaque, int width, int height);
protected CGLSurfaceData(CGLGraphicsConfig gc, ColorModel cm, int type,
int width, int height) {
super(gc, cm, type);
// TEXTURE shouldn't be scaled, it is used for managed BufferedImages.
scale = type == TEXTURE ? 1 : gc.getDevice().getScaleFactor();
this.width = width * scale;
this.height = height * scale;
}
protected CGLSurfaceData(CPlatformView pView, CGLGraphicsConfig gc,
ColorModel cm, int type)
ColorModel cm, int type,int width, int height)
{
super(gc, cm, type);
this(gc, cm, type, width, height);
this.pView = pView;
this.graphicsConfig = gc;
......@@ -70,9 +81,9 @@ public abstract class CGLSurfaceData extends OGLSurfaceData {
}
protected CGLSurfaceData(CGLLayer layer, CGLGraphicsConfig gc,
ColorModel cm, int type)
ColorModel cm, int type,int width, int height)
{
super(gc, cm, type);
this(gc, cm, type, width, height);
this.graphicsConfig = gc;
long pConfigInfo = gc.getNativeConfigInfo();
......@@ -157,13 +168,43 @@ public abstract class CGLSurfaceData extends OGLSurfaceData {
// Overridden in CGLWindowSurfaceData below
}
@Override
public int getDefaultScale() {
return scale;
}
@Override
public boolean copyArea(SunGraphics2D sg2d, int x, int y, int w, int h,
int dx, int dy) {
final int state = sg2d.transformState;
if (state > SunGraphics2D.TRANSFORM_TRANSLATESCALE
|| sg2d.compositeState >= SunGraphics2D.COMP_XOR) {
return false;
}
if (state <= SunGraphics2D.TRANSFORM_ANY_TRANSLATE) {
x += sg2d.transX;
y += sg2d.transY;
} else if (state == SunGraphics2D.TRANSFORM_TRANSLATESCALE) {
final double[] coords = {x, y, x + w, y + h, x + dx, y + dy};
sg2d.transform.transform(coords, 0, coords, 0, 3);
x = (int) Math.ceil(coords[0] - 0.5);
y = (int) Math.ceil(coords[1] - 0.5);
w = ((int) Math.ceil(coords[2] - 0.5)) - x;
h = ((int) Math.ceil(coords[3] - 0.5)) - y;
dx = ((int) Math.ceil(coords[4] - 0.5)) - x;
dy = ((int) Math.ceil(coords[5] - 0.5)) - y;
}
oglRenderPipe.copyArea(sg2d, x, y, w, h, dx, dy);
return true;
}
protected native void clearWindow();
public static class CGLWindowSurfaceData extends CGLSurfaceData {
public CGLWindowSurfaceData(CPlatformView pView,
CGLGraphicsConfig gc) {
super(pView, gc, gc.getColorModel(), WINDOW);
super(pView, gc, gc.getColorModel(), WINDOW, 0, 0);
}
@Override
......@@ -217,17 +258,12 @@ public abstract class CGLSurfaceData extends OGLSurfaceData {
public static class CGLLayerSurfaceData extends CGLSurfaceData {
private CGLLayer layer;
private int width, height;
public CGLLayerSurfaceData(CGLLayer layer, CGLGraphicsConfig gc,
int width, int height) {
super(layer, gc, gc.getColorModel(), FBOBJECT);
this.width = width;
this.height = height;
super(layer, gc, gc.getColorModel(), FBOBJECT, width, height);
this.layer = layer;
initSurface(width, height);
initSurface(this.width, this.height);
}
@Override
......@@ -296,18 +332,13 @@ public abstract class CGLSurfaceData extends OGLSurfaceData {
public static class CGLOffScreenSurfaceData extends CGLSurfaceData {
private Image offscreenImage;
private int width, height;
public CGLOffScreenSurfaceData(CPlatformView pView,
CGLGraphicsConfig gc, int width, int height, Image image,
ColorModel cm, int type) {
super(pView, gc, cm, type);
this.width = width;
this.height = height;
super(pView, gc, cm, type, width, height);
offscreenImage = image;
initSurface(width, height);
initSurface(this.width, this.height);
}
@Override
......
......@@ -463,35 +463,8 @@ public abstract class LWComponentPeer<T extends Component, D extends JComponent>
private void applyConstrain(final Graphics g) {
final SunGraphics2D sg2d = (SunGraphics2D) g;
final Rectangle constr = localToWindow(getSize());
// translate and set rectangle constrain.
sg2d.constrain(constr.x, constr.y, constr.width, constr.height);
// set region constrain.
//sg2d.constrain(getVisibleRegion());
SG2DConstraint(sg2d, getVisibleRegion());
}
//TODO Move this method to SG2D?
void SG2DConstraint(final SunGraphics2D sg2d, Region r) {
sg2d.constrainX = sg2d.transX;
sg2d.constrainY = sg2d.transY;
Region c = sg2d.constrainClip;
if ((sg2d.constrainX | sg2d.constrainY) != 0) {
r = r.getTranslatedRegion(sg2d.constrainX, sg2d.constrainY);
}
if (c == null) {
c = r;
} else {
c = c.getIntersection(r);
if (c == sg2d.constrainClip) {
// Common case to ignore
return;
}
}
sg2d.constrainClip = c;
//validateCompClip() forced call.
sg2d.setDevClip(r.getLoX(), r.getLoY(), r.getWidth(), r.getHeight());
final Rectangle size = localToWindow(getSize());
sg2d.constrain(size.x, size.y, size.width, size.height, getVisibleRegion());
}
public Region getVisibleRegion() {
......
......@@ -580,17 +580,16 @@ public class LWWindowPeer
setBounds(x, y, w, h, SET_BOUNDS, false, false);
// Second, update the graphics config and surface data
checkIfOnNewScreen();
if (resized) {
final boolean isNewDevice = updateGraphicsDevice();
if (resized || isNewDevice) {
replaceSurfaceData();
flushOnscreenGraphics();
}
// Third, COMPONENT_MOVED/COMPONENT_RESIZED/PAINT events
if (moved || invalid) {
handleMove(x, y, true);
}
if (resized || invalid) {
if (resized || invalid || isNewDevice) {
handleResize(w, h, true);
repaintPeer();
}
......@@ -610,7 +609,7 @@ public class LWWindowPeer
}
if (!isTextured()) {
if (g instanceof SunGraphics2D) {
SG2DConstraint((SunGraphics2D) g, getRegion());
((SunGraphics2D) g).constrain(0, 0, w, h, getRegion());
}
g.setColor(getBackground());
g.fillRect(0, 0, w, h);
......@@ -922,7 +921,7 @@ public class LWWindowPeer
}
// If window's graphics config is changed from the app code, the
// config correspond to the same device as before; when the window
// is moved by user, graphicsDevice is updated in checkIfOnNewScreen().
// is moved by user, graphicsDevice is updated in notifyReshape().
// In either case, there's nothing to do with screenOn here
graphicsConfig = gc;
}
......@@ -930,11 +929,14 @@ public class LWWindowPeer
return true;
}
private void checkIfOnNewScreen() {
/**
* Returns true if the GraphicsDevice has been changed, false otherwise.
*/
public boolean updateGraphicsDevice() {
GraphicsDevice newGraphicsDevice = platformWindow.getGraphicsDevice();
synchronized (getStateLock()) {
if (graphicsDevice == newGraphicsDevice) {
return;
return false;
}
graphicsDevice = newGraphicsDevice;
}
......@@ -942,13 +944,14 @@ public class LWWindowPeer
// TODO: DisplayChangedListener stuff
final GraphicsConfiguration newGC = newGraphicsDevice.getDefaultConfiguration();
if (!setGraphicsConfig(newGC)) return;
if (!setGraphicsConfig(newGC)) return false;
SunToolkit.executeOnEventHandlerThread(getTarget(), new Runnable() {
public void run() {
AWTAccessor.getComponentAccessor().setGraphicsConfiguration(getTarget(), newGC);
}
});
return true;
}
/*
......@@ -983,6 +986,7 @@ public class LWWindowPeer
oldData.flush();
}
}
flushOnscreenGraphics();
}
private void blitSurfaceData(final SurfaceData src, final SurfaceData dst) {
......@@ -990,14 +994,15 @@ public class LWWindowPeer
if (src != dst && src != null && dst != null
&& !(dst instanceof NullSurfaceData)
&& !(src instanceof NullSurfaceData)
&& src.getSurfaceType().equals(dst.getSurfaceType())) {
final Rectangle size = getSize();
&& src.getSurfaceType().equals(dst.getSurfaceType())
&& src.getDefaultScale() == dst.getDefaultScale()) {
final Rectangle size = src.getBounds();
final Blit blit = Blit.locate(src.getSurfaceType(),
CompositeType.Src,
dst.getSurfaceType());
if (blit != null) {
blit.Blit(src, dst, AlphaComposite.Src,
getRegion(), 0, 0, 0, 0, size.width, size.height);
blit.Blit(src, dst, AlphaComposite.Src, null, 0, 0, 0, 0,
size.width, size.height);
}
}
}
......
......@@ -860,8 +860,8 @@ public class CPlatformWindow extends CFRetainedResource implements PlatformWindo
}
}
private void flushBuffers() {
if (isVisible() && !nativeBounds.isEmpty()) {
void flushBuffers() {
if (isVisible() && !nativeBounds.isEmpty() && !isFullScreenMode) {
try {
LWCToolkit.invokeAndWait(new Runnable() {
@Override
......
......@@ -315,3 +315,34 @@ Java_sun_awt_CGraphicsDevice_nativeGetDisplayModes
return jreturnArray;
}
/*
* Class: sun_awt_CGraphicsDevice
* Method: nativeGetScaleFactor
* Signature: (I)D
*/
JNIEXPORT jdouble JNICALL
Java_sun_awt_CGraphicsDevice_nativeGetScaleFactor
(JNIEnv *env, jclass class, jint displayID)
{
__block jdouble ret = 1.0f;
JNF_COCOA_ENTER(env);
[ThreadUtilities performOnMainThreadWaiting:YES block:^(){
NSArray *screens = [NSScreen screens];
for (NSScreen *screen in screens) {
NSDictionary *screenInfo = [screen deviceDescription];
NSNumber *screenID = [screenInfo objectForKey:@"NSScreenNumber"];
if ([screenID pointerValue] == displayID){
if ([screen respondsToSelector:@selector(backingScaleFactor)]) {
ret = [screen backingScaleFactor];
}
break;
}
}
}];
JNF_COCOA_EXIT(env);
return ret;
}
......@@ -61,6 +61,19 @@ AWT_ASSERT_APPKIT_THREAD;
//Layer backed view
//self.needsDisplayOnBoundsChange = YES;
//self.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
//Disable CALayer's default animation
NSMutableDictionary * actions = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
[NSNull null], @"bounds",
[NSNull null], @"contents",
[NSNull null], @"contentsScale",
[NSNull null], @"onOrderIn",
[NSNull null], @"onOrderOut",
[NSNull null], @"sublayers",
nil];
self.actions = actions;
[actions release];
textureID = 0; // texture will be created by rendering pipe
target = 0;
......@@ -121,8 +134,12 @@ AWT_ASSERT_APPKIT_THREAD;
// Set the current context to the one given to us.
CGLSetCurrentContext(glContext);
glViewport(0, 0, textureWidth, textureHeight);
// Should clear the whole CALayer, because it can be larger than our texture.
glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
glViewport(0, 0, textureWidth, textureHeight);
JNIEnv *env = [ThreadUtilities getJNIEnv];
static JNF_CLASS_CACHE(jc_JavaLayer, "sun/java2d/opengl/CGLLayer");
static JNF_MEMBER_CACHE(jm_drawInCGLContext, jc_JavaLayer, "drawInCGLContext", "()V");
......@@ -168,7 +185,7 @@ JNF_COCOA_EXIT(env);
// Must be called under the RQ lock.
JNIEXPORT void JNICALL
Java_sun_java2d_opengl_CGLLayer_validate
(JNIEnv *env, jobject obj, jlong layerPtr, jobject surfaceData)
(JNIEnv *env, jclass cls, jlong layerPtr, jobject surfaceData)
{
CGLLayer *layer = OBJC(layerPtr);
......@@ -186,9 +203,21 @@ Java_sun_java2d_opengl_CGLLayer_validate
// Must be called on the AppKit thread and under the RQ lock.
JNIEXPORT void JNICALL
Java_sun_java2d_opengl_CGLLayer_blitTexture
(JNIEnv *env, jobject obj, jlong layerPtr)
(JNIEnv *env, jclass cls, jlong layerPtr)
{
CGLLayer *layer = jlong_to_ptr(layerPtr);
[layer blitTexture];
}
JNIEXPORT void JNICALL
Java_sun_java2d_opengl_CGLLayer_nativeSetScale
(JNIEnv *env, jclass cls, jlong layerPtr, jdouble scale)
{
JNF_COCOA_ENTER(env);
CGLLayer *layer = jlong_to_ptr(layerPtr);
[ThreadUtilities performOnMainThreadWaiting:NO block:^(){
layer.contentsScale = scale;
}];
JNF_COCOA_EXIT(env);
}
......@@ -31,6 +31,7 @@ import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.ImageCapabilities;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Iterator;
import sun.java2d.SurfaceData;
......@@ -287,4 +288,18 @@ public abstract class SurfaceManager {
flush(true);
}
}
/**
* Returns a scale factor of the image. This is utility method, which
* fetches information from the SurfaceData of the image.
*
* @see SurfaceData#getDefaultScale
*/
public static int getImageScale(final Image img) {
if (!(img instanceof VolatileImage)) {
return 1;
}
final SurfaceManager sm = getManager(img);
return sm.getPrimarySurfaceData().getDefaultScale();
}
}
......@@ -65,6 +65,8 @@ import java.awt.image.ImageObserver;
import java.awt.Transparency;
import java.awt.font.GlyphVector;
import java.awt.font.TextLayout;
import sun.awt.image.SurfaceManager;
import sun.font.FontDesignMetrics;
import sun.font.FontUtilities;
import sun.java2d.pipe.PixelDrawPipe;
......@@ -82,14 +84,12 @@ import sun.java2d.loops.CompositeType;
import sun.java2d.loops.SurfaceType;
import sun.java2d.loops.Blit;
import sun.java2d.loops.MaskFill;
import sun.font.FontManager;
import java.awt.font.FontRenderContext;
import sun.java2d.loops.XORComposite;
import sun.awt.ConstrainableGraphics;
import sun.awt.SunHints;
import java.util.Map;
import java.util.Iterator;
import sun.java2d.DestSurfaceProvider;
import sun.misc.PerformanceLogger;
import javax.tools.annotation.GenerateNativeHeader;
......@@ -207,13 +207,15 @@ public final class SunGraphics2D
public RenderingHints hints;
public Region constrainClip; // lightweight bounds
public Region constrainClip; // lightweight bounds in pixels
public int constrainX;
public int constrainY;
public Region clipRegion;
public Shape usrClip;
protected Region devClip; // Actual physical drawable
protected Region devClip; // Actual physical drawable in pixels
private final int devScale; // Actual physical scale factor
// cached state for text rendering
private boolean validFontInfo;
......@@ -256,6 +258,12 @@ public final class SunGraphics2D
validateColor();
devScale = sd.getDefaultScale();
if (devScale != 1) {
transform.setToScale(devScale, devScale);
invalidateTransform();
}
font = f;
if (font == null) {
font = defaultFont;
......@@ -320,38 +328,40 @@ public final class SunGraphics2D
/**
* Constrain rendering for lightweight objects.
*
* REMIND: This method will back off to the "workaround"
* of using translate and clipRect if the Graphics
* to be constrained has a complex transform. The
* drawback of the workaround is that the resulting
* clip and device origin cannot be "enforced".
*
* @exception IllegalStateException If the Graphics
* to be constrained has a complex transform.
*/
public void constrain(int x, int y, int w, int h) {
if ((x|y) != 0) {
public void constrain(int x, int y, int w, int h, Region region) {
if ((x | y) != 0) {
translate(x, y);
}
if (transformState >= TRANSFORM_TRANSLATESCALE) {
if (transformState > TRANSFORM_TRANSLATESCALE) {
clipRect(0, 0, w, h);
return;
}
x = constrainX = transX;
y = constrainY = transY;
w = Region.dimAdd(x, w);
h = Region.dimAdd(y, h);
// changes parameters according to the current scale and translate.
final double scaleX = transform.getScaleX();
final double scaleY = transform.getScaleY();
x = constrainX = (int) transform.getTranslateX();
y = constrainY = (int) transform.getTranslateY();
w = Region.dimAdd(x, Region.clipScale(w, scaleX));
h = Region.dimAdd(y, Region.clipScale(h, scaleY));
Region c = constrainClip;
if (c == null) {
c = Region.getInstanceXYXY(x, y, w, h);
} else {
c = c.getIntersectionXYXY(x, y, w, h);
if (c == constrainClip) {
// Common case to ignore
return;
}
}
if (region != null) {
region = region.getScaledRegion(scaleX, scaleY);
region = region.getTranslatedRegion(x, y);
c = c.getIntersection(region);
}
if (c == constrainClip) {
// Common case to ignore
return;
}
constrainClip = c;
if (!devClip.isInsideQuickCheck(c)) {
devClip = devClip.getIntersection(c);
......@@ -359,6 +369,23 @@ public final class SunGraphics2D
}
}
/**
* Constrain rendering for lightweight objects.
*
* REMIND: This method will back off to the "workaround"
* of using translate and clipRect if the Graphics
* to be constrained has a complex transform. The
* drawback of the workaround is that the resulting
* clip and device origin cannot be "enforced".
*
* @exception IllegalStateException If the Graphics
* to be constrained has a complex transform.
*/
@Override
public void constrain(int x, int y, int w, int h) {
constrain(x, y, w, h, null);
}
protected static ValidatePipe invalidpipe = new ValidatePipe();
/*
......@@ -1540,11 +1567,13 @@ public final class SunGraphics2D
* @see TransformChain
* @see AffineTransform
*/
@Override
public void setTransform(AffineTransform Tx) {
if ((constrainX|constrainY) == 0) {
if ((constrainX | constrainY) == 0 && devScale == 1) {
transform.setTransform(Tx);
} else {
transform.setToTranslation(constrainX, constrainY);
transform.setTransform(devScale, 0, 0, devScale, constrainX,
constrainY);
transform.concatenate(Tx);
}
invalidateTransform();
......@@ -1602,12 +1631,15 @@ public final class SunGraphics2D
* @see #transform
* @see #setTransform
*/
@Override
public AffineTransform getTransform() {
if ((constrainX|constrainY) == 0) {
if ((constrainX | constrainY) == 0 && devScale == 1) {
return new AffineTransform(transform);
}
AffineTransform tx =
AffineTransform.getTranslateInstance(-constrainX, -constrainY);
final double invScale = 1.0 / devScale;
AffineTransform tx = new AffineTransform(invScale, 0, 0, invScale,
-constrainX * invScale,
-constrainY * invScale);
tx.concatenate(transform);
return tx;
}
......@@ -2991,6 +3023,37 @@ public final class SunGraphics2D
}
// end of text rendering methods
private static boolean isHiDPIImage(final Image img) {
return SurfaceManager.getImageScale(img) != 1;
}
private boolean drawHiDPIImage(Image img, int dx1, int dy1, int dx2,
int dy2, int sx1, int sy1, int sx2, int sy2,
Color bgcolor, ImageObserver observer) {
final int scale = SurfaceManager.getImageScale(img);
sx1 = Region.clipScale(sx1, scale);
sx2 = Region.clipScale(sx2, scale);
sy1 = Region.clipScale(sy1, scale);
sy2 = Region.clipScale(sy2, scale);
try {
return imagepipe.scaleImage(this, img, dx1, dy1, dx2, dy2, sx1, sy1,
sx2, sy2, bgcolor, observer);
} catch (InvalidPipeException e) {
try {
revalidateAll();
return imagepipe.scaleImage(this, img, dx1, dy1, dx2, dy2, sx1,
sy1, sx2, sy2, bgcolor, observer);
} catch (InvalidPipeException e2) {
// Still catching the exception; we are not yet ready to
// validate the surfaceData correctly. Fail for now and
// try again next time around.
return false;
}
} finally {
surfaceData.markDirty();
}
}
/**
* Draws an image scaled to x,y,w,h in nonblocking mode with a
* callback object.
......@@ -3004,8 +3067,9 @@ public final class SunGraphics2D
* Not part of the advertised API but a useful utility method
* to call internally. This is for the case where we are
* drawing to/from given coordinates using a given width/height,
* but we guarantee that the weidth/height of the src and dest
* areas are equal (no scale needed).
* but we guarantee that the surfaceData's width/height of the src and dest
* areas are equal (no scale needed). Note that this method intentionally
* ignore scale factor of the source image, and copy it as is.
*/
public boolean copyImage(Image img, int dx, int dy, int sx, int sy,
int width, int height, Color bgcolor,
......@@ -3043,7 +3107,15 @@ public final class SunGraphics2D
if ((width == 0) || (height == 0)) {
return true;
}
if (width == img.getWidth(null) && height == img.getHeight(null)) {
final int imgW = img.getWidth(null);
final int imgH = img.getHeight(null);
if (isHiDPIImage(img)) {
return drawHiDPIImage(img, x, y, x + width, y + height, 0, 0, imgW,
imgH, bg, observer);
}
if (width == imgW && height == imgH) {
return copyImage(img, x, y, 0, 0, width, height, bg, observer);
}
......@@ -3084,6 +3156,13 @@ public final class SunGraphics2D
return true;
}
if (isHiDPIImage(img)) {
final int imgW = img.getWidth(null);
final int imgH = img.getHeight(null);
return drawHiDPIImage(img, x, y, x + imgW, y + imgH, 0, 0, imgW,
imgH, bg, observer);
}
try {
return imagepipe.copyImage(this, img, x, y, bg, observer);
} catch (InvalidPipeException e) {
......@@ -3132,6 +3211,11 @@ public final class SunGraphics2D
return true;
}
if (isHiDPIImage(img)) {
return drawHiDPIImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2,
bgcolor, observer);
}
if (((sx2 - sx1) == (dx2 - dx1)) &&
((sy2 - sy1) == (dy2 - dy1)))
{
......@@ -3210,6 +3294,18 @@ public final class SunGraphics2D
return drawImage(img, 0, 0, null, observer);
}
if (isHiDPIImage(img)) {
final int w = img.getWidth(null);
final int h = img.getHeight(null);
final AffineTransform tx = new AffineTransform(transform);
transform(xform);
boolean result = drawHiDPIImage(img, 0, 0, w, h, 0, 0, w, h, null,
observer);
transform.setTransform(tx);
invalidateTransform();
return result;
}
try {
return imagepipe.transformImage(this, img, xform, observer);
} catch (InvalidPipeException e) {
......
......@@ -1057,4 +1057,14 @@ public abstract class SurfaceData
* responsible for returning the appropriate object.
*/
public abstract Object getDestination();
/**
* Returns default scale factor of the destination surface. Scale factor
* describes the mapping between virtual and physical coordinates of the
* SurfaceData. If the scale is 2 then virtual pixel coordinates need to be
* doubled for physical pixels.
*/
public int getDefaultScale() {
return 1;
}
}
/*
* Copyright (c) 2005, 2008, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
......@@ -99,8 +99,7 @@ public abstract class BufferedContext {
private int validatedRGB;
private int validatedFlags;
private boolean xformInUse;
private int transX;
private int transY;
private AffineTransform transform;
protected BufferedContext(RenderQueue rq) {
this.rq = rq;
......@@ -277,14 +276,11 @@ public abstract class BufferedContext {
resetTransform();
xformInUse = false;
txChanged = true;
} else if (sg2d != null) {
if (transX != sg2d.transX || transY != sg2d.transY) {
txChanged = true;
}
} else if (sg2d != null && !sg2d.transform.equals(transform)) {
txChanged = true;
}
if (sg2d != null) {
transX = sg2d.transX;
transY = sg2d.transY;
if (sg2d != null && txChanged) {
transform = new AffineTransform(sg2d.transform);
}
} else {
setTransform(xform);
......
......@@ -27,9 +27,7 @@ package sun.java2d.pipe;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
......@@ -38,15 +36,13 @@ import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DirectColorModel;
import java.awt.image.ImageObserver;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.VolatileImage;
import java.awt.image.WritableRaster;
import java.awt.image.ImagingOpException;
import sun.awt.SunHints;
import sun.awt.image.ImageRepresentation;
import sun.awt.image.SurfaceManager;
import sun.awt.image.ToolkitImage;
import sun.java2d.InvalidPipeException;
import sun.java2d.SunGraphics2D;
......@@ -323,15 +319,17 @@ public class DrawImage implements DrawImagePipe
BufferedImage makeBufferedImage(Image img, Color bgColor, int type,
int sx1, int sy1, int sx2, int sy2)
{
BufferedImage bimg = new BufferedImage(sx2-sx1, sy2-sy1, type);
Graphics2D g2d = bimg.createGraphics();
final int width = sx2 - sx1;
final int height = sy2 - sy1;
final BufferedImage bimg = new BufferedImage(width, height, type);
final SunGraphics2D g2d = (SunGraphics2D) bimg.createGraphics();
g2d.setComposite(AlphaComposite.Src);
if (bgColor != null) {
g2d.setColor(bgColor);
g2d.fillRect(0, 0, sx2-sx1, sy2-sy1);
g2d.fillRect(0, 0, width, height);
g2d.setComposite(AlphaComposite.SrcOver);
}
g2d.drawImage(img, -sx1, -sy1, null);
g2d.copyImage(img, 0, 0, sx1, sy1, width, height, null, null);
g2d.dispose();
return bimg;
}
......@@ -737,8 +735,9 @@ public class DrawImage implements DrawImagePipe
atfm.scale(m00, m11);
atfm.translate(srcX-sx1, srcY-sy1);
int imgW = img.getWidth(null);
int imgH = img.getHeight(null);
final int scale = SurfaceManager.getImageScale(img);
final int imgW = img.getWidth(null) * scale;
final int imgH = img.getHeight(null) * scale;
srcW += srcX;
srcH += srcY;
// Make sure we are not out of bounds
......
......@@ -131,6 +131,28 @@ public class Region {
return newv;
}
/**
* Multiply the scale factor {@code sv} and the value {@code v} with
* appropriate clipping to the bounds of Integer resolution. If the answer
* would be greater than {@code Integer.MAX_VALUE} then {@code
* Integer.MAX_VALUE} is returned. If the answer would be less than {@code
* Integer.MIN_VALUE} then {@code Integer.MIN_VALUE} is returned. Otherwise
* the multiplication is returned.
*/
public static int clipScale(final int v, final double sv) {
if (sv == 1.0) {
return v;
}
final double newv = v * sv;
if (newv < Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
if (newv > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
return (int) Math.round(newv);
}
protected Region(int lox, int loy, int hix, int hiy) {
this.lox = lox;
this.loy = loy;
......@@ -348,6 +370,79 @@ public class Region {
calcBBox();
}
/**
* Returns a Region object that represents the same list of rectangles as
* the current Region object, scaled by the specified sx, sy factors.
*/
public Region getScaledRegion(final double sx, final double sy) {
if (sx == 0 || sy == 0 || this == EMPTY_REGION) {
return EMPTY_REGION;
}
if ((sx == 1.0 && sy == 1.0) || (this == WHOLE_REGION)) {
return this;
}
int tlox = clipScale(lox, sx);
int tloy = clipScale(loy, sy);
int thix = clipScale(hix, sx);
int thiy = clipScale(hiy, sy);
Region ret = new Region(tlox, tloy, thix, thiy);
int bands[] = this.bands;
if (bands != null) {
int end = endIndex;
int newbands[] = new int[end];
int i = 0; // index for source bands
int j = 0; // index for translated newbands
int ncol;
while (i < end) {
int y1, y2;
newbands[j++] = y1 = clipScale(bands[i++], sy);
newbands[j++] = y2 = clipScale(bands[i++], sy);
newbands[j++] = ncol = bands[i++];
int savej = j;
if (y1 < y2) {
while (--ncol >= 0) {
int x1 = clipScale(bands[i++], sx);
int x2 = clipScale(bands[i++], sx);
if (x1 < x2) {
newbands[j++] = x1;
newbands[j++] = x2;
}
}
} else {
i += ncol * 2;
}
// Did we get any non-empty bands in this row?
if (j > savej) {
newbands[savej-1] = (j - savej) / 2;
} else {
j = savej - 3;
}
}
if (j <= 5) {
if (j < 5) {
// No rows or bands were generated...
ret.lox = ret.loy = ret.hix = ret.hiy = 0;
} else {
// Only generated one single rect in the end...
ret.loy = newbands[0];
ret.hiy = newbands[1];
ret.lox = newbands[3];
ret.hix = newbands[4];
}
// ret.endIndex and ret.bands were never initialized...
// ret.endIndex = 0;
// ret.newbands = null;
} else {
// Generated multiple bands and/or multiple rows...
ret.endIndex = j;
ret.bands = newbands;
}
}
return ret;
}
/**
* Returns a Region object that represents the same list of
* rectangles as the current Region object, translated by
......
/*
* Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.TexturePaint;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
/**
* @test
* @bug 8000629
* @summary TexturePaint areas shouldn't separates.
* @author Sergey Bylokhov
*/
public class FillTexturePaint {
private static TexturePaint shape;
private static final int size = 400;
static {
BufferedImage bi = new BufferedImage(50, 50,
BufferedImage.TYPE_INT_RGB);
Graphics2D gi = bi.createGraphics();
gi.setBackground(Color.GREEN);
gi.clearRect(0, 0, 50, 50);
shape = new TexturePaint(bi, new Rectangle(0, 0, 50, 50));
}
public static void main(final String[] args) {
GraphicsEnvironment ge =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsConfiguration gc =
ge.getDefaultScreenDevice().getDefaultConfiguration();
VolatileImage vi = gc.createCompatibleVolatileImage(size, size);
while (true) {
vi.validate(gc);
Graphics2D g2d = vi.createGraphics();
g2d.setComposite(AlphaComposite.Src);
g2d.setPaint(shape);
g2d.fill(new Rectangle(0, 0, size, size));
g2d.dispose();
if (vi.validate(gc) != VolatileImage.IMAGE_OK) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
continue;
}
BufferedImage bi = vi.getSnapshot();
if (vi.contentsLost()) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
continue;
}
for (int x = 0; x < size; ++x) {
for (int y = 0; y < size; ++y) {
if (bi.getRGB(x, y) != Color.GREEN.getRGB()) {
throw new RuntimeException("Test failed.");
}
}
}
break;
}
}
}
/*
* Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
/**
* @test
* @bug 8000629
* @author Sergey Bylokhov
*/
public final class FlipDrawImage {
private static final int width = 400;
private static final int height = 400;
public static void main(final String[] args) {
GraphicsEnvironment ge =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsConfiguration gc =
ge.getDefaultScreenDevice().getDefaultConfiguration();
VolatileImage vi = gc.createCompatibleVolatileImage(width, height);
final BufferedImage bi = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
while (true) {
vi.validate(gc);
Graphics2D g2d = vi.createGraphics();
g2d.setColor(Color.red);
g2d.fillRect(0, 0, width, height);
g2d.setColor(Color.green);
g2d.fillRect(0, 0, width / 2, height / 2);
g2d.dispose();
if (vi.validate(gc) != VolatileImage.IMAGE_OK) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
continue;
}
Graphics2D g = bi.createGraphics();
g.setComposite(AlphaComposite.Src);
g.setColor(Color.BLUE);
g.fillRect(0, 0, width, height);
// destination width and height are flipped and scale is used.
g.drawImage(vi, width / 2, height / 2, -width / 2, -height / 2,
null, null);
g.dispose();
if (vi.contentsLost()) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
continue;
}
for (int x = 0; x < width; ++x) {
for (int y = 0; y < height; ++y) {
if (x < width / 2 && y < height / 2) {
if (x >= width / 4 && y >= height / 4) {
if (bi.getRGB(x, y) != Color.green.getRGB()) {
throw new RuntimeException("Test failed.");
}
} else if (bi.getRGB(x, y) != Color.red.getRGB()) {
throw new RuntimeException("Test failed.");
}
} else {
if (bi.getRGB(x, y) != Color.BLUE.getRGB()) {
throw new RuntimeException("Test failed.");
}
}
}
}
break;
}
}
}
/*
* Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.geom.AffineTransform;
import java.awt.image.VolatileImage;
import sun.java2d.SunGraphics2D;
/**
* @test
* @bug 8000629
* @summary Set/get transform should work on constrained graphics.
* @author Sergey Bylokhov
*/
public class TransformSetGet {
public static void main(final String[] args) {
final GraphicsEnvironment ge =
GraphicsEnvironment.getLocalGraphicsEnvironment();
final GraphicsConfiguration gc =
ge.getDefaultScreenDevice().getDefaultConfiguration();
final VolatileImage vi = gc.createCompatibleVolatileImage(200, 200);
final SunGraphics2D sg2d = (SunGraphics2D) vi.createGraphics();
sg2d.constrain(0, 61, 100, 100);
final AffineTransform expected = sg2d.cloneTransform();
sg2d.setTransform(sg2d.getTransform());
final AffineTransform actual = sg2d.cloneTransform();
sg2d.dispose();
vi.flush();
if (!expected.equals(actual)) {
System.out.println("Expected = " + expected);
System.out.println("Actual = " + actual);
throw new RuntimeException("Wrong transform");
}
}
}
/*
* Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
/**
* @test
* @bug 8000629
* @summary Temporary backbuffer in the DrawImage should not fill background
* outside of source image bounds.
* @author Sergey Bylokhov
*/
public final class IncorrectBounds {
private static final int width = 400;
private static final int height = 400;
public static void main(final String[] args) {
GraphicsEnvironment ge =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsConfiguration gc =
ge.getDefaultScreenDevice().getDefaultConfiguration();
VolatileImage vi = gc.createCompatibleVolatileImage(width / 4,
height / 4);
final BufferedImage bi = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
while (true) {
vi.validate(gc);
Graphics2D g2d = vi.createGraphics();
g2d.setColor(Color.green);
g2d.fillRect(0, 0, width / 4, height / 4);
g2d.dispose();
if (vi.validate(gc) != VolatileImage.IMAGE_OK) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
continue;
}
Graphics2D g = bi.createGraphics();
g.setComposite(AlphaComposite.Src);
g.setColor(Color.red);
g.fillRect(0, 0, width, height);
// Use sx and sy outside of VI bounds.
g.drawImage(vi, 0, 0, width / 2, height / 2, 0, 0, width * 2,
height * 2, null);
g.dispose();
if (vi.contentsLost()) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
continue;
}
for (int x = 0; x < width; ++x) {
for (int y = 0; y < height; ++y) {
if (x < width / 16 && y < height / 16) {
if (bi.getRGB(x, y) != Color.green.getRGB()) {
throw new RuntimeException("Test failed.");
}
} else {
if (bi.getRGB(x, y) != Color.red.getRGB()) {
throw new RuntimeException("Test failed.");
}
}
}
}
break;
}
}
}
/*
* Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
/**
* @test
* @bug 8000629
* @summary Temporary backbuffer in the DrawImage should have correct offset.
* @author Sergey Bylokhov
*/
public final class IncorrectOffset {
private static final int width = 400;
private static final int height = 400;
public static void main(final String[] args) {
GraphicsEnvironment ge =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsConfiguration gc =
ge.getDefaultScreenDevice().getDefaultConfiguration();
VolatileImage vi = gc.createCompatibleVolatileImage(width, height);
BufferedImage bi = new BufferedImage(width / 4, height / 4,
BufferedImage.TYPE_INT_ARGB);
while (true) {
vi.validate(gc);
Graphics2D g2d = vi.createGraphics();
g2d.setColor(Color.black);
g2d.fillRect(0, 0, width, height);
g2d.setColor(Color.green);
g2d.fillRect(width / 4, height / 4, width / 2, height / 2);
g2d.dispose();
if (vi.validate(gc) != VolatileImage.IMAGE_OK) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
continue;
}
Graphics2D g = bi.createGraphics();
g.setComposite(AlphaComposite.Src);
// Scale part of VI to BI. Only green area should be copied.
g.drawImage(vi, 0, 0, width / 4, height / 4, width / 4, height / 4,
width / 4 + width / 2, height / 4 + height / 2, null);
g.dispose();
if (vi.contentsLost()) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
continue;
}
for (int x = 0; x < width / 4; ++x) {
for (int y = 0; y < height / 4; ++y) {
if (bi.getRGB(x, y) != Color.green.getRGB()) {
throw new RuntimeException("Test failed.");
}
}
}
break;
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册