提交 aa627fd2 编写于 作者: B bae

8023794: [macosx] LCD Rendering hints seems not working without FRACTIONALMETRICS=ON

Reviewed-by: serb, prr
上级 4cffa7cc
......@@ -366,8 +366,7 @@ public final class LWCToolkit extends LWToolkit {
protected void initializeDesktopProperties() {
super.initializeDesktopProperties();
Map <Object, Object> fontHints = new HashMap<>();
fontHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
fontHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
fontHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
desktopProperties.put(SunToolkit.DESKTOPFONTHINTS, fontHints);
desktopProperties.put("awt.mouse.numButtons", BUTTONS);
......
......@@ -311,21 +311,26 @@ JNF_COCOA_ENTER(env);
jlong *glyphInfos =
(*env)->GetPrimitiveArrayCritical(env, glyphInfoLongArray, NULL);
if (glyphInfos != NULL) {
jint *rawGlyphCodes =
(*env)->GetPrimitiveArrayCritical(env, glyphCodes, NULL);
jint *rawGlyphCodes =
(*env)->GetPrimitiveArrayCritical(env, glyphCodes, NULL);
@try {
if (rawGlyphCodes != NULL && glyphInfos != NULL) {
CGGlyphImages_GetGlyphImagePtrs(glyphInfos, awtStrike,
rawGlyphCodes, len);
}
}
@finally {
if (rawGlyphCodes != NULL) {
CGGlyphImages_GetGlyphImagePtrs(glyphInfos, awtStrike,
rawGlyphCodes, len);
(*env)->ReleasePrimitiveArrayCritical(env, glyphCodes,
rawGlyphCodes, JNI_ABORT);
(*env)->ReleasePrimitiveArrayCritical(env, glyphCodes,
rawGlyphCodes, JNI_ABORT);
}
if (glyphInfos != NULL) {
// Do not use JNI_COMMIT, as that will not free the buffer copy
// when +ProtectJavaHeap is on.
(*env)->ReleasePrimitiveArrayCritical(env, glyphInfoLongArray,
glyphInfos, 0);
}
// Do not use JNI_COMMIT, as that will not free the buffer copy
// when +ProtectJavaHeap is on.
(*env)->ReleasePrimitiveArrayCritical(env, glyphInfoLongArray,
glyphInfos, 0);
}
JNF_COCOA_EXIT(env);
......
......@@ -195,19 +195,41 @@ DUMP_GLYPHINFO(const GlyphInfo *info)
#pragma mark --- Font Rendering Mode Descriptors ---
static Int32 reverseGamma = 0;
static UInt8 reverseGammaLut[256] = { 0 };
static inline UInt8* getReverseGammaLut() {
if (reverseGamma == 0) {
// initialize gamma lut
double gamma;
int i;
const char* pGammaEnv = getenv("J2D_LCD_REVERSE_GAMMA");
if (pGammaEnv != NULL) {
reverseGamma = atol(pGammaEnv);
}
if (reverseGamma < 100 || reverseGamma > 250) {
reverseGamma = 180;
}
gamma = 100.0 / reverseGamma;
for (i = 0; i < 256; i++) {
double x = ((double)i) / 255.0;
reverseGammaLut[i] = (UInt8)(255 * pow(x, gamma));
}
}
return reverseGammaLut;
}
static inline void
CGGI_CopyARGBPixelToRGBPixel(const UInt32 p, UInt8 *dst)
{
#if __LITTLE_ENDIAN__
*(dst + 2) = 0xFF - (p >> 24 & 0xFF);
*(dst + 1) = 0xFF - (p >> 16 & 0xFF);
*(dst) = 0xFF - (p >> 8 & 0xFF);
#else
*(dst) = 0xFF - (p >> 16 & 0xFF);
*(dst + 1) = 0xFF - (p >> 8 & 0xFF);
*(dst + 2) = 0xFF - (p & 0xFF);
#endif
UInt8* lut = getReverseGammaLut();
*(dst + 0) = lut[0xFF - (p >> 16 & 0xFF)]; // red
*(dst + 1) = lut[0xFF - (p >> 8 & 0xFF)]; // green
*(dst + 2) = lut[0xFF - (p & 0xFF)]; // blue
}
static void
......@@ -222,17 +244,14 @@ CGGI_CopyImageFromCanvasToRGBInfo(CGGI_GlyphCanvas *canvas, GlyphInfo *info)
size_t height = info->height;
size_t y;
// fill empty glyph image with black-on-white glyph
for (y = 0; y < height; y++) {
size_t destRow = y * destRowWidth * 3;
size_t srcRow = y * srcRowWidth;
size_t x;
for (x = 0; x < destRowWidth; x++) {
// size_t x3 = x * 3;
// UInt32 p = src[srcRow + x];
// dest[destRow + x3] = 0xFF - (p >> 16 & 0xFF);
// dest[destRow + x3 + 1] = 0xFF - (p >> 8 & 0xFF);
// dest[destRow + x3 + 2] = 0xFF - (p & 0xFF);
CGGI_CopyARGBPixelToRGBPixel(src[srcRow + x],
dest + destRow + x * 3);
}
......@@ -260,13 +279,9 @@ CGGI_CopyImageFromCanvasToRGBInfo(CGGI_GlyphCanvas *canvas, GlyphInfo *info)
//}
static inline UInt8
CGGI_ConvertPixelToGreyBit(UInt32 p)
CGGI_ConvertBWPixelToByteGray(UInt32 p)
{
#ifdef __LITTLE_ENDIAN__
return 0xFF - ((p >> 24 & 0xFF) + (p >> 16 & 0xFF) + (p >> 8 & 0xFF)) / 3;
#else
return 0xFF - ((p >> 16 & 0xFF) + (p >> 8 & 0xFF) + (p & 0xFF)) / 3;
#endif
return 0xFF - (((p >> 24 & 0xFF) + (p >> 16 & 0xFF) + (p >> 8 & 0xFF)) / 3);
}
static void
......@@ -281,14 +296,15 @@ CGGI_CopyImageFromCanvasToAlphaInfo(CGGI_GlyphCanvas *canvas, GlyphInfo *info)
size_t height = info->height;
size_t y;
// fill empty glyph image with black-on-white glyph
for (y = 0; y < height; y++) {
size_t destRow = y * destRowWidth;
size_t srcRow = y * srcRowWidth;
size_t x;
for (x = 0; x < destRowWidth; x++) {
UInt32 p = src[srcRow + x];
dest[destRow + x] = CGGI_ConvertPixelToGreyBit(p);
dest[destRow + x] = CGGI_ConvertBWPixelToByteGray(p);
}
}
}
......@@ -316,13 +332,11 @@ CGGI_GetRenderingMode(const AWTStrike *strike)
{
CGGI_RenderingMode mode;
mode.cgFontMode = strike->fStyle;
NSException *e = nil;
switch (strike->fAAStyle) {
case sun_awt_SunHints_INTVAL_TEXT_ANTIALIAS_DEFAULT:
case sun_awt_SunHints_INTVAL_TEXT_ANTIALIAS_OFF:
case sun_awt_SunHints_INTVAL_TEXT_ANTIALIAS_ON:
case sun_awt_SunHints_INTVAL_TEXT_ANTIALIAS_GASP:
default:
mode.glyphDescriptor = &grey;
break;
case sun_awt_SunHints_INTVAL_TEXT_ANTIALIAS_LCD_HRGB:
......@@ -331,6 +345,17 @@ CGGI_GetRenderingMode(const AWTStrike *strike)
case sun_awt_SunHints_INTVAL_TEXT_ANTIALIAS_LCD_VBGR:
mode.glyphDescriptor = &rgb;
break;
case sun_awt_SunHints_INTVAL_TEXT_ANTIALIAS_GASP:
case sun_awt_SunHints_INTVAL_TEXT_ANTIALIAS_DEFAULT:
default:
/* we expect that text antialiasing hint has been already
* evaluated. Report an error if we get 'unevaluated' hint here.
*/
e = [NSException
exceptionWithName:@"IllegalArgumentException"
reason:@"Invalid hint value"
userInfo:nil];
@throw e;
}
return mode;
......@@ -345,7 +370,8 @@ CGGI_GetRenderingMode(const AWTStrike *strike)
*/
static inline void
CGGI_InitCanvas(CGGI_GlyphCanvas *canvas,
const vImagePixelCount width, const vImagePixelCount height)
const vImagePixelCount width, const vImagePixelCount height,
const CGGI_RenderingMode* mode)
{
// our canvas is *always* 4-byte ARGB
size_t bytesPerRow = width * sizeof(UInt32);
......@@ -356,19 +382,26 @@ CGGI_InitCanvas(CGGI_GlyphCanvas *canvas,
canvas->image->height = height;
canvas->image->rowBytes = bytesPerRow;
canvas->image->data = (void *)calloc(byteCount, sizeof(UInt32));
canvas->image->data = (void *)calloc(byteCount, sizeof(UInt8));
if (canvas->image->data == NULL) {
[[NSException exceptionWithName:NSMallocException
reason:@"Failed to allocate memory for the buffer which backs the CGContext for glyph strikes." userInfo:nil] raise];
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
uint32_t bmpInfo = kCGImageAlphaPremultipliedFirst;
if (mode->glyphDescriptor == &rgb) {
bmpInfo |= kCGBitmapByteOrder32Host;
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
canvas->context = CGBitmapContextCreate(canvas->image->data,
width, height, 8, bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedFirst);
bmpInfo);
// set foreground color
CGContextSetRGBFillColor(canvas->context, 0.0f, 0.0f, 0.0f, 1.0f);
CGContextSetFontSize(canvas->context, 1);
CGContextSaveGState(canvas->context);
......@@ -404,7 +437,9 @@ CGGI_FreeCanvas(CGGI_GlyphCanvas *canvas)
* Quick and easy inline to check if this canvas is big enough.
*/
static inline void
CGGI_SizeCanvas(CGGI_GlyphCanvas *canvas, const vImagePixelCount width, const vImagePixelCount height, const JRSFontRenderingStyle style)
CGGI_SizeCanvas(CGGI_GlyphCanvas *canvas, const vImagePixelCount width,
const vImagePixelCount height,
const CGGI_RenderingMode* mode)
{
if (canvas->image != NULL &&
width < canvas->image->width &&
......@@ -418,8 +453,9 @@ CGGI_SizeCanvas(CGGI_GlyphCanvas *canvas, const vImagePixelCount width, const vI
CGGI_FreeCanvas(canvas);
CGGI_InitCanvas(canvas,
width * CGGI_GLYPH_CANVAS_SLACK,
height * CGGI_GLYPH_CANVAS_SLACK);
JRSFontSetRenderingStyleOnContext(canvas->context, style);
height * CGGI_GLYPH_CANVAS_SLACK,
mode);
JRSFontSetRenderingStyleOnContext(canvas->context, mode->cgFontMode);
}
/*
......@@ -443,6 +479,7 @@ CGGI_ClearCanvas(CGGI_GlyphCanvas *canvas, GlyphInfo *info)
Pixel_8888 opaqueWhite = { 0xFF, 0xFF, 0xFF, 0xFF };
#endif
// clear canvas background and set foreground color
vImageBufferFill_ARGB8888(&canvasRectToClear, opaqueWhite, kvImageNoFlags);
}
......@@ -577,7 +614,7 @@ CGGI_CreateImageForUnicode
GlyphInfo *info = CGGI_CreateNewGlyphInfoFrom(advance, bbox, strike, mode);
// fix the context size, just in case the substituted character is unexpectedly large
CGGI_SizeCanvas(canvas, info->width, info->height, mode->cgFontMode);
CGGI_SizeCanvas(canvas, info->width, info->height, mode);
// align the transform for the real CoreText strike
CGContextSetTextMatrix(canvas->context, strike->fAltTx);
......@@ -653,8 +690,11 @@ CGGI_FillImagesForGlyphsWithSizedCanvas(CGGI_GlyphCanvas *canvas,
#endif
}
static NSString *threadLocalCanvasKey =
@"Java CoreGraphics Text Renderer Cached Canvas";
static NSString *threadLocalAACanvasKey =
@"Java CoreGraphics Text Renderer Cached Canvas for AA";
static NSString *threadLocalLCDCanvasKey =
@"Java CoreGraphics Text Renderer Cached Canvas for LCD";
/*
* This is the maximum length and height times the above slack squared
......@@ -678,25 +718,28 @@ CGGI_FillImagesForGlyphs(jlong *glyphInfos, const AWTStrike *strike,
CGGI_GLYPH_CANVAS_MAX*CGGI_GLYPH_CANVAS_MAX*CGGI_GLYPH_CANVAS_SLACK*CGGI_GLYPH_CANVAS_SLACK)
{
CGGI_GlyphCanvas *tmpCanvas = [[CGGI_GlyphCanvas alloc] init];
CGGI_InitCanvas(tmpCanvas, maxWidth, maxHeight);
CGGI_InitCanvas(tmpCanvas, maxWidth, maxHeight, mode);
CGGI_FillImagesForGlyphsWithSizedCanvas(tmpCanvas, strike,
mode, glyphInfos, uniChars,
glyphs, len);
mode, glyphInfos, uniChars,
glyphs, len);
CGGI_FreeCanvas(tmpCanvas);
[tmpCanvas release];
return;
}
NSMutableDictionary *threadDict =
[[NSThread currentThread] threadDictionary];
CGGI_GlyphCanvas *canvas = [threadDict objectForKey:threadLocalCanvasKey];
NSString* theKey = (mode->glyphDescriptor == &rgb) ?
threadLocalLCDCanvasKey : threadLocalAACanvasKey;
CGGI_GlyphCanvas *canvas = [threadDict objectForKey:theKey];
if (canvas == nil) {
canvas = [[CGGI_GlyphCanvas alloc] init];
[threadDict setObject:canvas forKey:threadLocalCanvasKey];
[threadDict setObject:canvas forKey:theKey];
}
CGGI_SizeCanvas(canvas, maxWidth, maxHeight, mode->cgFontMode);
CGGI_SizeCanvas(canvas, maxWidth, maxHeight, mode);
CGGI_FillImagesForGlyphsWithSizedCanvas(canvas, strike, mode,
glyphInfos, uniChars, glyphs, len);
}
......
......@@ -26,6 +26,7 @@
package sun.java2d.opengl;
import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.Transparency;
......@@ -400,8 +401,8 @@ public abstract class OGLSurfaceData extends SurfaceData
/**
* For now, we can only render LCD text if:
* - the fragment shader extension is available, and
* - blending is disabled, and
* - the source color is opaque
* - the source color is opaque, and
* - blending is SrcOverNoEa or disabled
* - and the destination is opaque
*
* Eventually, we could enhance the native OGL text rendering code
......@@ -411,9 +412,19 @@ public abstract class OGLSurfaceData extends SurfaceData
public boolean canRenderLCDText(SunGraphics2D sg2d) {
return
graphicsConfig.isCapPresent(CAPS_EXT_LCD_SHADER) &&
sg2d.compositeState <= SunGraphics2D.COMP_ISCOPY &&
sg2d.surfaceData.getTransparency() == Transparency.OPAQUE &&
sg2d.paintState <= SunGraphics2D.PAINT_OPAQUECOLOR &&
sg2d.surfaceData.getTransparency() == Transparency.OPAQUE;
(sg2d.compositeState <= SunGraphics2D.COMP_ISCOPY ||
(sg2d.compositeState <= SunGraphics2D.COMP_ALPHA && canHandleComposite(sg2d.composite)));
}
private boolean canHandleComposite(Composite c) {
if (c instanceof AlphaComposite) {
AlphaComposite ac = (AlphaComposite)c;
return ac.getRule() == AlphaComposite.SRC_OVER && ac.getAlpha() >= 1f;
}
return false;
}
public void validatePipe(SunGraphics2D sg2d) {
......
......@@ -748,7 +748,7 @@ OGLContext_IsLCDShaderSupportAvailable(JNIEnv *env,
// finally, check to see if the hardware supports the required number
// of texture units
j2d_glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS_ARB, &maxTexUnits);
if (maxTexUnits < 4) {
if (maxTexUnits < 2) {
J2dRlsTraceLn1(J2D_TRACE_INFO,
"OGLContext_IsLCDShaderSupportAvailable: not enough tex units (%d)",
maxTexUnits);
......
......@@ -275,12 +275,9 @@ OGLTR_AddToGlyphCache(GlyphInfo *glyph, jboolean rgbOrder)
* changes, we will modify the "src_adj" value in OGLTR_UpdateLCDTextColor()).
*
* The "main" function is executed for each "fragment" (or pixel) in the
* glyph image. We have determined that the pow() function can be quite
* slow and it only operates on scalar values, not vectors as we require.
* So instead we build two 3D textures containing gamma (and inverse gamma)
* lookup tables that allow us to approximate a component-wise pow() function
* with a single 3D texture lookup. This approach is at least 2x faster
* than the equivalent pow() calls.
* glyph image. The pow() routine operates on vectors, gives precise results,
* and provides acceptable level of performance, so we use it to perform
* the gamma adjustment.
*
* The variables involved in the equation can be expressed as follows:
*
......@@ -299,8 +296,8 @@ static const char *lcdTextShaderSource =
"uniform vec3 src_adj;"
"uniform sampler2D glyph_tex;"
"uniform sampler2D dst_tex;"
"uniform sampler3D invgamma_tex;"
"uniform sampler3D gamma_tex;"
"uniform vec3 gamma;"
"uniform vec3 invgamma;"
""
"void main(void)"
"{"
......@@ -312,12 +309,12 @@ static const char *lcdTextShaderSource =
" }"
// load the RGB value from the corresponding destination pixel
" vec3 dst_clr = vec3(texture2D(dst_tex, gl_TexCoord[1].st));"
// gamma adjust the dest color using the invgamma LUT
" vec3 dst_adj = vec3(texture3D(invgamma_tex, dst_clr.stp));"
// gamma adjust the dest color
" vec3 dst_adj = pow(dst_clr.rgb, gamma);"
// linearly interpolate the three color values
" vec3 result = mix(dst_adj, src_adj, glyph_clr);"
// gamma re-adjust the resulting color (alpha is always set to 1.0)
" gl_FragColor = vec4(vec3(texture3D(gamma_tex, result.stp)), 1.0);"
" gl_FragColor = vec4(pow(result.rgb, invgamma), 1.0);"
"}";
/**
......@@ -348,10 +345,6 @@ OGLTR_CreateLCDTextProgram()
j2d_glUniform1iARB(loc, 0); // texture unit 0
loc = j2d_glGetUniformLocationARB(lcdTextProgram, "dst_tex");
j2d_glUniform1iARB(loc, 1); // texture unit 1
loc = j2d_glGetUniformLocationARB(lcdTextProgram, "invgamma_tex");
j2d_glUniform1iARB(loc, 2); // texture unit 2
loc = j2d_glGetUniformLocationARB(lcdTextProgram, "gamma_tex");
j2d_glUniform1iARB(loc, 3); // texture unit 3
// "unuse" the program object; it will be re-bound later as needed
j2d_glUseProgramObjectARB(0);
......@@ -360,108 +353,26 @@ OGLTR_CreateLCDTextProgram()
}
/**
* Initializes a 3D texture object for use as a three-dimensional gamma
* lookup table. Note that the wrap mode is initialized to GL_LINEAR so
* that the table will interpolate adjacent values when the index falls
* somewhere in between.
*/
static GLuint
OGLTR_InitGammaLutTexture()
{
GLuint lutTextureID;
j2d_glGenTextures(1, &lutTextureID);
j2d_glBindTexture(GL_TEXTURE_3D, lutTextureID);
j2d_glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
j2d_glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
j2d_glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
j2d_glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
j2d_glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
return lutTextureID;
}
/**
* Updates the lookup table in the given texture object with the float
* values in the given system memory buffer. Note that we could use
* glTexSubImage3D() when updating the texture after its first
* initialization, but since we're updating the entire table (with
* power-of-two dimensions) and this is a relatively rare event, we'll
* just stick with glTexImage3D().
*/
static void
OGLTR_UpdateGammaLutTexture(GLuint texID, GLfloat *lut, jint size)
{
j2d_glBindTexture(GL_TEXTURE_3D, texID);
j2d_glTexImage3D(GL_TEXTURE_3D, 0, GL_RGB8,
size, size, size, 0, GL_RGB, GL_FLOAT, lut);
}
/**
* (Re)Initializes the gamma lookup table textures.
* (Re)Initializes the gamma related uniforms.
*
* The given contrast value is an int in the range [100, 250] which we will
* then scale to fit in the range [1.0, 2.5]. We create two LUTs, one
* that essentially calculates pow(x, gamma) and the other calculates
* pow(x, 1/gamma). These values are replicated in all three dimensions, so
* given a single 3D texture coordinate (typically this will be a triplet
* in the form (r,g,b)), the 3D texture lookup will return an RGB triplet:
*
* (pow(r,g), pow(y,g), pow(z,g)
*
* where g is either gamma or 1/gamma, depending on the table.
* then scale to fit in the range [1.0, 2.5].
*/
static jboolean
OGLTR_UpdateLCDTextContrast(jint contrast)
{
double gamma = ((double)contrast) / 100.0;
double ig = gamma;
double g = 1.0 / ig;
GLfloat lut[LUT_EDGE][LUT_EDGE][LUT_EDGE][3];
GLfloat invlut[LUT_EDGE][LUT_EDGE][LUT_EDGE][3];
int min = 0;
int max = LUT_EDGE - 1;
int x, y, z;
double g = ((double)contrast) / 100.0;
double ig = 1.0 / g;
GLint loc;
J2dTraceLn1(J2D_TRACE_INFO,
"OGLTR_UpdateLCDTextContrast: contrast=%d", contrast);
for (z = min; z <= max; z++) {
double zval = ((double)z) / max;
GLfloat gz = (GLfloat)pow(zval, g);
GLfloat igz = (GLfloat)pow(zval, ig);
for (y = min; y <= max; y++) {
double yval = ((double)y) / max;
GLfloat gy = (GLfloat)pow(yval, g);
GLfloat igy = (GLfloat)pow(yval, ig);
loc = j2d_glGetUniformLocationARB(lcdTextProgram, "gamma");
j2d_glUniform3fARB(loc, g, g, g);
for (x = min; x <= max; x++) {
double xval = ((double)x) / max;
GLfloat gx = (GLfloat)pow(xval, g);
GLfloat igx = (GLfloat)pow(xval, ig);
lut[z][y][x][0] = gx;
lut[z][y][x][1] = gy;
lut[z][y][x][2] = gz;
invlut[z][y][x][0] = igx;
invlut[z][y][x][1] = igy;
invlut[z][y][x][2] = igz;
}
}
}
if (gammaLutTextureID == 0) {
gammaLutTextureID = OGLTR_InitGammaLutTexture();
}
OGLTR_UpdateGammaLutTexture(gammaLutTextureID, (GLfloat *)lut, LUT_EDGE);
if (invGammaLutTextureID == 0) {
invGammaLutTextureID = OGLTR_InitGammaLutTexture();
}
OGLTR_UpdateGammaLutTexture(invGammaLutTextureID,
(GLfloat *)invlut, LUT_EDGE);
loc = j2d_glGetUniformLocationARB(lcdTextProgram, "invgamma");
j2d_glUniform3fARB(loc, ig, ig, ig);
return JNI_TRUE;
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册