Changed FBSurface API to access/modify source and destination rendering

rectangles.  Now that the underlying rendering is hardware accelerated,
this API more closely matches what is actually occurring.  As well, it
makes things easier comprehend: 'src' rect is the actual surface data,
and 'dst' rect is its final output onscreen (scaling, etc being applied).


git-svn-id: svn://svn.code.sf.net/p/stella/code/trunk@2883 8b62c5a3-ac7e-4cc8-8f21-d9a121418aba
This commit is contained in:
stephena 2014-05-04 16:59:11 +00:00
parent 0110cd829e
commit b418d5a4f5
13 changed files with 212 additions and 152 deletions

View File

@ -17,7 +17,6 @@
// $Id$
//============================================================================
#include "Font.hxx"
#include "FBSurfaceSDL2.hxx"
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -34,12 +33,17 @@ FBSurfaceSDL2::FBSurfaceSDL2(FrameBufferSDL2& buffer, uInt32 width, uInt32 heigh
mySurface = SDL_CreateRGBSurface(0, width, height,
pf->BitsPerPixel, pf->Rmask, pf->Gmask, pf->Bmask, pf->Amask);
mySrc.x = mySrc.y = myDst.x = myDst.y = 0;
mySrc.w = myDst.w = width;
mySrc.h = myDst.h = height;
// We start out with the src and dst rectangles containing the same
// dimensions, indicating no scaling or re-positioning
mySrcR.x = mySrcR.y = myDstR.x = myDstR.y = 0;
mySrcR.w = myDstR.w = width;
mySrcR.h = myDstR.h = height;
////////////////////////////////////////////////////
// These *must* be set for the parent class
myPixels = (uInt32*) mySurface->pixels;
myPitch = mySurface->pitch / pf->BytesPerPixel;
////////////////////////////////////////////////////
// To generate texture
reload();
@ -63,7 +67,7 @@ void FBSurfaceSDL2::fillRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h, uInt32 colo
tmp.y = y;
tmp.w = w;
tmp.h = h;
SDL_FillRect(mySurface, &tmp, myFB.myDefPalette[color]);
SDL_FillRect(mySurface, &tmp, myPalette[color]);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -74,10 +78,10 @@ void FBSurfaceSDL2::drawSurface(const FBSurface* surface, uInt32 tx, uInt32 ty)
SDL_Rect dst;
dst.x = tx;
dst.y = ty;
dst.w = s->mySrc.w;
dst.h = s->mySrc.h;
dst.w = s->mySrcR.w;
dst.h = s->mySrcR.h;
SDL_BlitSurface(s->mySurface, &(s->mySrc), mySurface, &dst);
SDL_BlitSurface(s->mySurface, &(s->mySrcR), mySurface, &dst);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -88,44 +92,51 @@ void FBSurfaceSDL2::addDirtyRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h)
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurfaceSDL2::getPos(uInt32& x, uInt32& y) const
void FBSurfaceSDL2::basePtr(uInt32*& pixels, uInt32& pitch)
{
x = myDst.x;
y = myDst.y;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurfaceSDL2::setPos(uInt32 x, uInt32 y)
GUI::Rect FBSurfaceSDL2::srcRect()
{
myDst.x = x;
myDst.y = y;
return GUI::Rect(mySrcR.x, mySrcR.y, mySrcR.x+mySrcR.w, mySrcR.y+mySrcR.h);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurfaceSDL2::setWidth(uInt32 w)
GUI::Rect FBSurfaceSDL2::dstRect()
{
// This method can't be used with 'scaled' surface (aka TIA surfaces)
// That shouldn't really matter, though, as all the UI stuff isn't scaled,
// and it's the only thing that uses it
mySrc.w = w;
myDst.w = w;
return GUI::Rect(myDstR.x, myDstR.y, myDstR.x+myDstR.w, myDstR.y+myDstR.h);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurfaceSDL2::setHeight(uInt32 h)
void FBSurfaceSDL2::setSrcPos(uInt32 x, uInt32 y)
{
// This method can't be used with 'scaled' surface (aka TIA surfaces)
// That shouldn't really matter, though, as all the UI stuff isn't scaled,
// and it's the only thing that uses it
mySrc.h = h;
myDst.h = h;
mySrcR.x = x; mySrcR.y = y;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurfaceSDL2::setSrcSize(uInt32 w, uInt32 h)
{
mySrcR.w = w; mySrcR.h = h;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurfaceSDL2::setDstPos(uInt32 x, uInt32 y)
{
myDstR.x = x; myDstR.y = y;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurfaceSDL2::setDstSize(uInt32 w, uInt32 h)
{
myDstR.w = w; myDstR.w = w;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurfaceSDL2::translateCoords(Int32& x, Int32& y) const
{
x -= myDst.x;
y -= myDst.y;
x -= myDstR.x;
y -= myDstR.y;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -133,11 +144,11 @@ void FBSurfaceSDL2::render()
{
if(mySurfaceIsDirty)
{
//cerr << "src: x=" << mySrc.x << ", y=" << mySrc.y << ", w=" << mySrc.w << ", h=" << mySrc.h << endl;
//cerr << "dst: x=" << myDst.x << ", y=" << myDst.y << ", w=" << myDst.w << ", h=" << myDst.h << endl;
//cerr << "src: x=" << mySrcR.x << ", y=" << mySrcR.y << ", w=" << mySrcR.w << ", h=" << mySrcR.h << endl;
//cerr << "dst: x=" << myDstR.x << ", y=" << myDstR.y << ", w=" << myDstR.w << ", h=" << myDstR.h << endl;
SDL_UpdateTexture(myTexture, &mySrc, mySurface->pixels, mySurface->pitch);
SDL_RenderCopy(myFB.myRenderer, myTexture, &mySrc, &myDst);
SDL_UpdateTexture(myTexture, &mySrcR, mySurface->pixels, mySurface->pitch);
SDL_RenderCopy(myFB.myRenderer, myTexture, &mySrcR, &myDstR);
mySurfaceIsDirty = false;

View File

@ -24,32 +24,33 @@
#include "FrameBufferSDL2.hxx"
/**
A surface suitable for SDL Render2D API, making use of hardware
acceleration behind the scenes. This class extends
FrameBuffer::FBSurface.
An FBSurface suitable for the SDL2 Render2D API, making use of hardware
acceleration behind the scenes.
@author Stephen Anthony
*/
class FBSurfaceSDL2 : public FBSurface
{
friend class FrameBufferSDL2;
public:
FBSurfaceSDL2(FrameBufferSDL2& buffer, uInt32 width, uInt32 height);
virtual ~FBSurfaceSDL2();
// Most of the surface drawing primitives are defined in FBSurface;
// the ones defined here use SDL-specific code
// Most of the surface drawing primitives are implemented in FBSurface;
// the ones implemented here use SDL-specific code for extra performance
//
void fillRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h, uInt32 color);
void drawSurface(const FBSurface* surface, uInt32 x, uInt32 y);
void addDirtyRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h);
void getPos(uInt32& x, uInt32& y) const;
void setPos(uInt32 x, uInt32 y);
uInt32 getWidth() const { return mySrc.w; }
uInt32 getHeight() const { return mySrc.h; }
void setWidth(uInt32 w);
void setHeight(uInt32 h);
void basePtr(uInt32*& pixels, uInt32& pitch);
GUI::Rect srcRect();
GUI::Rect dstRect();
void setSrcPos(uInt32 x, uInt32 y);
void setSrcSize(uInt32 w, uInt32 h);
void setDstPos(uInt32 x, uInt32 y);
void setDstSize(uInt32 w, uInt32 h);
void translateCoords(Int32& x, Int32& y) const;
void render();
void invalidate();
@ -61,7 +62,7 @@ class FBSurfaceSDL2 : public FBSurface
SDL_Surface* mySurface;
SDL_Texture* myTexture;
SDL_Rect mySrc, myDst;
SDL_Rect mySrcR, myDstR;
bool mySurfaceIsDirty;
};

View File

@ -42,15 +42,33 @@ class FBSurfaceTIA : public FBSurface
// TIA surfaces don't implement most of the drawing primitives,
// only the methods absolutely necessary for dealing with drawing
// a TIA image
void getPos(uInt32& x, uInt32& y) const;
uInt32 getWidth() const { return mySrcR.w; }
uInt32 getHeight() const { return mySrcR.h; }
void translateCoords(Int32& x, Int32& y) const;
void render();
void invalidate();
void free();
void reload();
void basePtr(uInt32*& pixels, uInt32& pitch) { }
GUI::Rect srcRect() { return GUI::Rect(); }
GUI::Rect dstRect() { return GUI::Rect(); }
///////////////////////////////////////////////////////
void getPos(uInt32& x, uInt32& y) const;
uInt32 getWidth() const { return myDstR.w; }
uInt32 getHeight() const { return myDstR.h; }
///////////////////////////////////////////////////////
void setSrcPos(uInt32 x, uInt32 y) { }
void setSrcSize(uInt32 w, uInt32 h) { }
void setDstPos(uInt32 x, uInt32 y) { }
void setDstSize(uInt32 w, uInt32 h) { }
///////////////////////////////////////////////////////
void setPos(uInt32 x, uInt32 y) { }
void setWidth(uInt32 w) { }
void setHeight(uInt32 h) { }
///////////////////////////////////////////////////////
private:
void setTIA(const TIA& tia) { myTIA = &tia; }
void setTIAPalette(const uInt32* palette);

View File

@ -310,6 +310,7 @@ bool PNGLibrary::allocateStorage(png_uint_32 w, png_uint_32 h)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void PNGLibrary::scaleImagetoSurface(const FrameBuffer& fb, FBSurface& surface)
{
#if 0 //FIXSDL
// Figure out the original zoom level of the snapshot
// All snapshots generated by Stella are at most some multiple of 320
// pixels wide
@ -356,6 +357,7 @@ void PNGLibrary::scaleImagetoSurface(const FrameBuffer& fb, FBSurface& surface)
while(ystride--)
surface.drawPixels(ReadInfo.line, 0, srow++, sw);
}
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

View File

@ -113,7 +113,7 @@ void RomListSettings::center()
if(x + _w > tx) x -= (x + _w - tx);
if(y + _h > ty) y -= (y + _h - ty);
surface().setPos(x, y);
surface().setDstPos(x, y);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

View File

@ -22,11 +22,11 @@
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FBSurface::FBSurface(const uInt32* palette)
: myPixels(NULL),
myPitch(0),
myPalette(palette)
: myPalette(palette),
myPixels(NULL),
myPitch(0)
{
// NOTE: myPixels and myPitch will be set in child classes that inherit
// NOTE: myPixels and myPitch MUST be set in child classes that inherit
// from this class
}
@ -49,6 +49,13 @@ void FBSurface::vLine(uInt32 x, uInt32 y, uInt32 y2, uInt32 color)
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurface::fillRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h, uInt32 color)
{
while(h--)
hLine(x, y+h, x+w-1, color);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurface::drawChar(const GUI::Font& font, uInt8 chr,
uInt32 tx, uInt32 ty, uInt32 color)
@ -122,6 +129,11 @@ void FBSurface::drawPixels(uInt32* data, uInt32 tx, uInt32 ty, uInt32 numpixels)
*buffer++ = (uInt32) data[i];
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurface::addDirtyRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h)
{
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurface::box(uInt32 x, uInt32 y, uInt32 w, uInt32 h,
uInt32 colorA, uInt32 colorB)

View File

@ -24,6 +24,7 @@ class FrameBuffer;
#include "bspf.hxx"
#include "Font.hxx"
#include "Rect.hxx"
/**
This class is basically a thin wrapper around the video toolkit 'surface'
@ -61,6 +62,23 @@ class FBSurface
*/
virtual ~FBSurface() { }
/**
This method returns the surface pixel pointer and pitch, which are
used when one wishes to modify the surface pixels directly.
*/
inline void basePtr(uInt32*& pixels, uInt32& pitch)
{
pixels = myPixels;
pitch = myPitch;
}
//////////////////////////////////////////////////////////////////////////
// Note: The drawing primitives below will work, but do not take
// advantage of any acceleration whatsoever. The methods are
// marked as 'virtual' so that child classes can choose to
// implement them more efficiently.
//////////////////////////////////////////////////////////////////////////
/**
This method should be called to draw a horizontal line.
@ -91,7 +109,7 @@ class FBSurface
@param color The fill color of the rectangle
*/
virtual void fillRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h,
uInt32 color) { }
uInt32 color);
/**
This method should be called to draw the specified character.
@ -128,85 +146,6 @@ class FBSurface
*/
virtual void drawPixels(uInt32* data, uInt32 x, uInt32 y, uInt32 numpixels);
/**
This method should be called copy the contents of the given
surface into the FrameBuffer surface.
@param surface The data to draw
@param x The x coordinate
@param y The y coordinate
*/
virtual void drawSurface(const FBSurface* surface, uInt32 x, uInt32 y) { }
/**
This method should be called to add a dirty rectangle
(ie, an area of the screen that has changed)
@param x The x coordinate
@param y The y coordinate
@param w The width of the area
@param h The height of the area
*/
virtual void addDirtyRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h) { }
/**
This method answers the current position of the surface.
*/
virtual void getPos(uInt32& x, uInt32& y) const { }
/**
This method should be called to set the position of the surface.
*/
virtual void setPos(uInt32 x, uInt32 y) { }
/**
This method answers the current dimensions of the surface.
*/
virtual uInt32 getWidth() const { return 0; }
virtual uInt32 getHeight() const { return 0; }
/**
This method sets the width of the drawable area of the surface.
*/
virtual void setWidth(uInt32 w) { }
/**
This method sets the width of the drawable area of the surface.
*/
virtual void setHeight(uInt32 h) { }
/**
This method should be called to translate the given coordinates
to the surface coordinates.
@param x X coordinate to translate
@param y Y coordinate to translate
*/
virtual void translateCoords(Int32& x, Int32& y) const { }
/**
This method should be called to draw the surface to the screen.
*/
virtual void render() { }
/**
This method should be called to reset the surface to empty
pixels / colour black.
*/
virtual void invalidate() { }
/**
This method should be called to free any resources being used by
the surface.
*/
virtual void free() { }
/**
This method should be called to reload the surface data/state.
It will normally be called after free().
*/
virtual void reload() { }
/**
This method should be called to draw a rectangular box with sides
at the specified coordinates.
@ -222,14 +161,15 @@ class FBSurface
uInt32 colorA, uInt32 colorB);
/**
This method should be called to draw a framed rectangle.
I'm not exactly sure what it is, so I can't explain it :)
This method should be called to draw a framed rectangle with
several different possible styles.
@param x The x coordinate
@param y The y coordinate
@param w The width of the area
@param h The height of the area
@param color The color of the surrounding frame
@param style The 'FrameStyle' to use for the surrounding frame
*/
virtual void frameRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h,
uInt32 color, FrameStyle style = kSolidLine);
@ -253,13 +193,87 @@ class FBSurface
uInt32 color, TextAlignment align = kTextAlignLeft,
int deltax = 0, bool useEllipsis = true);
/**
This method should be called copy the contents of the given
surface into the FrameBuffer surface.
@param surface The data to draw
@param x The x coordinate
@param y The y coordinate
*/
virtual void drawSurface(const FBSurface* surface, uInt32 x, uInt32 y) { }
/**
This method should be called to add a dirty rectangle
(ie, an area of the screen that has changed)
@param x The x coordinate
@param y The y coordinate
@param w The width of the area
@param h The height of the area
*/
virtual void addDirtyRect(uInt32 x, uInt32 y, uInt32 w, uInt32 h);
//////////////////////////////////////////////////////////////////////////
// Note: The following methods are FBSurface-specific, and must be
// implemented in child classes.
//
// For the following, 'src' indicates the actual data buffer area
// (non-scaled) and 'dst' indicates the rendered area (possibly scaled).
//////////////////////////////////////////////////////////////////////////
/**
These methods answer the current dimensions of the specified surface.
*/
virtual GUI::Rect srcRect() = 0;
virtual GUI::Rect dstRect() = 0;
/**
These methods set the origin point and width/height for the
specified service. They are defined as separate x/y and w/h
methods since these items are sometimes set separately.
*/
virtual void setSrcPos(uInt32 x, uInt32 y) = 0;
virtual void setSrcSize(uInt32 w, uInt32 h) = 0;
virtual void setDstPos(uInt32 x, uInt32 y) = 0;
virtual void setDstSize(uInt32 w, uInt32 h) = 0;
/**
This method should be called to translate the given coordinates
to the (destination) surface coordinates.
@param x X coordinate to translate
@param y Y coordinate to translate
*/
virtual void translateCoords(Int32& x, Int32& y) const = 0;
/**
This method should be called to draw the surface to the screen.
*/
virtual void render() = 0;
/**
This method should be called to reset the surface to empty
pixels / colour black.
*/
virtual void invalidate() = 0;
/**
This method should be called to free any resources being used by
the surface.
*/
virtual void free() = 0;
/**
This method should be called to reload the surface data/state.
It will normally be called after free().
*/
virtual void reload() = 0;
protected:
// These are also used by any derived FBSurface classes
const uInt32* myPalette;
uInt32* myPixels;
uInt32 myPitch;
private:
const uInt32* myPalette;
};
#endif

View File

@ -231,7 +231,7 @@ FBInitStatus FrameBuffer::createDisplay(const string& title,
myStatsMsg.w = infoFont().getMaxCharWidth() * 24 + 2;
myStatsMsg.h = (infoFont().getFontHeight() + 2) * 2;
if(myStatsMsg.surface == NULL)
if(myStatsMsg.surface == NULL)
{
uInt32 surfaceID = allocateSurface(myStatsMsg.w, myStatsMsg.h);
myStatsMsg.surface = surface(surfaceID);
@ -296,7 +296,7 @@ void FrameBuffer::update()
myStatsMsg.surface->drawString(infoFont(),
info.BankSwitch, 1, 15, myStatsMsg.w, myStatsMsg.color, kTextAlignLeft);
myStatsMsg.surface->addDirtyRect(0, 0, 0, 0); // force a full draw
myStatsMsg.surface->setPos(myImageRect.x() + 1, myImageRect.y() + 1);
myStatsMsg.surface->setDstPos(myImageRect.x() + 1, myImageRect.y() + 1);
myStatsMsg.surface->render();
}
break; // S_EMULATE
@ -388,8 +388,7 @@ void FrameBuffer::showMessage(const string& message, MessagePosition position,
myMsg.w = font().getStringWidth(myMsg.text) + 10;
myMsg.h = font().getFontHeight() + 8;
myMsg.surface->setWidth(myMsg.w);
myMsg.surface->setHeight(myMsg.h);
myMsg.surface->setDstSize(myMsg.w, myMsg.h);
myMsg.position = position;
myMsg.enabled = true;
}
@ -481,7 +480,7 @@ inline void FrameBuffer::drawMessage()
break;
}
myMsg.surface->setPos(myMsg.x + myImageRect.x(), myMsg.y + myImageRect.y());
myMsg.surface->setDstPos(myMsg.x + myImageRect.x(), myMsg.y + myImageRect.y());
myMsg.surface->fillRect(1, 1, myMsg.w-2, myMsg.h-2, kBtnColor);
myMsg.surface->box(0, 0, myMsg.w, myMsg.h, kColor, kShadowColor);
myMsg.surface->drawString(font(), myMsg.text, 4, 4,

View File

@ -102,7 +102,7 @@ void ContextMenu::center()
if(x + _w > tx) x -= (x + _w - tx);
if(y + _h > ty) y -= (y + _h - ty);
surface().setPos(x, y);
surface().setDstPos(x, y);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

View File

@ -122,7 +122,7 @@ void Dialog::center()
const GUI::Size& screen = instance().frameBuffer().screenSize();
uInt32 x = (screen.w - getWidth()) >> 1;
uInt32 y = (screen.h - getHeight()) >> 1;
_surface->setPos(x, y);
_surface->setDstPos(x, y);
}
}

View File

@ -148,7 +148,7 @@ void InputTextDialog::center()
if(x + _w > tx) x -= (x + _w - tx);
if(y + _h > ty) y -= (y + _h - ty);
surface().setPos(x, y);
surface().setDstPos(x, y);
}
else
Dialog::center();

View File

@ -79,8 +79,8 @@ void PopUpWidget::handleMouseDown(int x, int y, int button, int clickCount)
{
// Add menu just underneath parent widget
const GUI::Rect& image = instance().frameBuffer().imageRect();
uInt32 tx, ty;
dialog().surface().getPos(tx, ty);
const GUI::Rect& srect = dialog().surface().dstRect();
uInt32 tx = srect.x(), ty = srect.y();
tx += getAbsX() + _labelWidth - image.x();
ty += getAbsY() + getHeight() - image.y();
myMenu->show(tx, ty, myMenu->getSelected());

View File

@ -86,6 +86,7 @@ void RomInfoWidget::clearProperties()
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void RomInfoWidget::parseProperties()
{
#if 0 //FIXSDL
// Check if a surface has ever been created; if so, we use it
// The surface will always be the maximum size, but sometimes we'll
// only draw certain parts of it
@ -131,6 +132,7 @@ void RomInfoWidget::parseProperties()
myRomInfo.push_back("Note: " + myProperties.get(Cartridge_Note));
myRomInfo.push_back("Controllers: " + myProperties.get(Controller_Left) +
" (left), " + myProperties.get(Controller_Right) + " (right)");
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -148,8 +150,9 @@ void RomInfoWidget::drawWidget(bool hilite)
if(mySurfaceIsValid)
{
uInt32 x = _x + ((_w - mySurface->getWidth()) >> 1);
uInt32 y = _y + ((yoff - mySurface->getHeight()) >> 1);
const GUI::Rect& r = mySurface->dstRect();
uInt32 x = _x + ((_w - r.width()) >> 1);
uInt32 y = _y + ((yoff - r.height()) >> 1);
s.drawSurface(mySurface, x, y);
}
else if(mySurfaceErrorMsg != "")