1.] GL Textures dump to User/Dumps/Textures instead of picking a dir 2.] Some code cleanup in OGL plugin 3.] Added new dump feature "Dump EFB Target", its a bit unstable and needs a fix.
git-svn-id: https://dolphin-emu.googlecode.com/svn/trunk@2593 8ced0084-cf51-0410-be5f-012b33b47a6e
This commit is contained in:
parent
316c311b8a
commit
9e3db8d1b5
|
@ -68,6 +68,7 @@
|
|||
#define STATESAVES_DIR "StateSaves"
|
||||
#define SCREENSHOTS_DIR "ScreenShots"
|
||||
#define DUMP_DIR "Dump"
|
||||
#define DUMP_TEXTURES_DIR "Textures"
|
||||
#define LOGS_DIR "Logs"
|
||||
#define MAIL_LOGS_DIR "Mail"
|
||||
|
||||
|
@ -122,6 +123,7 @@
|
|||
#define FULL_STATESAVES_DIR FULL_USERDATA_DIR STATESAVES_DIR DIR_SEP
|
||||
#define FULL_SCREENSHOTS_DIR FULL_USERDATA_DIR SCREENSHOTS_DIR DIR_SEP
|
||||
#define FULL_DUMP_DIR FULL_USERDATA_DIR DUMP_DIR DIR_SEP
|
||||
#define FULL_DUMP_TEXTURES_DIR FULL_USERDATA_DIR DUMP_DIR DIR_SEP DUMP_TEXTURES_DIR
|
||||
#define FULL_LOGS_DIR FULL_USERDATA_DIR LOGS_DIR DIR_SEP
|
||||
#define FULL_MAIL_LOGS_DIR FULL_LOGS_DIR MAIL_LOGS_DIR DIR_SEP
|
||||
#define FULL_MAPS_DIR FULL_USERDATA_DIR MAPS_DIR DIR_SEP
|
||||
|
|
|
@ -58,18 +58,11 @@ void Config::Load()
|
|||
iniFile.Get("Settings", "OverlayProjStats", &bOverlayProjStats, false);
|
||||
iniFile.Get("Settings", "DLOptimize", &iCompileDLsLevel, 0);
|
||||
iniFile.Get("Settings", "DumpTextures", &bDumpTextures, 0);
|
||||
iniFile.Get("Settings", "DumpEFBTarget", &bDumpEFBTarget, 0);
|
||||
iniFile.Get("Settings", "ShowShaderErrors", &bShowShaderErrors, 0);
|
||||
iniFile.Get("Settings", "Multisample", &iMultisampleMode, 0);
|
||||
if (iMultisampleMode == 0)
|
||||
iMultisampleMode = 1;
|
||||
std::string s;
|
||||
iniFile.Get("Settings", "TexDumpPath", &s, 0);
|
||||
if (s.size() < sizeof(texDumpPath) )
|
||||
strcpy(texDumpPath, s.c_str());
|
||||
else {
|
||||
strncpy(texDumpPath, s.c_str(), sizeof(texDumpPath)-1);
|
||||
texDumpPath[sizeof(texDumpPath) - 1] = 0;
|
||||
}
|
||||
|
||||
iniFile.Get("Settings", "TexFmtOverlayEnable", &bTexFmtOverlayEnable, 0);
|
||||
iniFile.Get("Settings", "TexFmtOverlayCenter", &bTexFmtOverlayCenter, 0);
|
||||
|
@ -110,9 +103,9 @@ void Config::Save()
|
|||
iniFile.Set("Settings", "OverlayProjStats", bOverlayProjStats);
|
||||
iniFile.Set("Settings", "DLOptimize", iCompileDLsLevel);
|
||||
iniFile.Set("Settings", "DumpTextures", bDumpTextures);
|
||||
iniFile.Set("Settings", "DumpEFBTarget", bDumpEFBTarget);
|
||||
iniFile.Set("Settings", "ShowShaderErrors", bShowShaderErrors);
|
||||
iniFile.Set("Settings", "Multisample", iMultisampleMode);
|
||||
iniFile.Set("Settings", "TexDumpPath", texDumpPath);
|
||||
iniFile.Set("Settings", "TexFmtOverlayEnable", bTexFmtOverlayEnable);
|
||||
iniFile.Set("Settings", "TexFmtOverlayCenter", bTexFmtOverlayCenter);
|
||||
iniFile.Set("Settings", "Wireframe", bWireFrame);
|
||||
|
|
|
@ -68,8 +68,8 @@ struct Config
|
|||
bool bDisableTexturing;
|
||||
|
||||
// Utility
|
||||
char texDumpPath[280];
|
||||
bool bDumpTextures;
|
||||
bool bDumpEFBTarget;
|
||||
|
||||
// Hacks
|
||||
bool bEFBCopyDisable;
|
||||
|
|
|
@ -52,13 +52,13 @@ BEGIN_EVENT_TABLE(ConfigDialog,wxDialog)
|
|||
EVT_CHECKBOX(ID_TEXFMTOVERLAY, ConfigDialog::AdvancedSettingsChanged)
|
||||
EVT_CHECKBOX(ID_TEXFMTCENTER, ConfigDialog::AdvancedSettingsChanged)
|
||||
EVT_CHECKBOX(ID_DUMPTEXTURES, ConfigDialog::AdvancedSettingsChanged)
|
||||
EVT_CHECKBOX(ID_DUMPEFBTARGET, ConfigDialog::AdvancedSettingsChanged)
|
||||
EVT_CHECKBOX(ID_DISABLELIGHTING, ConfigDialog::AdvancedSettingsChanged)
|
||||
EVT_CHECKBOX(ID_DISABLETEXTURING, ConfigDialog::AdvancedSettingsChanged)
|
||||
EVT_CHECKBOX(ID_EFBCOPYDISABLEHOTKEY, ConfigDialog::AdvancedSettingsChanged)
|
||||
EVT_CHECKBOX(ID_PROJECTIONHACK1,ConfigDialog::AdvancedSettingsChanged)
|
||||
EVT_CHECKBOX(ID_SAFETEXTURECACHE,ConfigDialog::AdvancedSettingsChanged)
|
||||
EVT_CHECKBOX(ID_CHECKBOX_DISABLECOPYEFB, ConfigDialog::AdvancedSettingsChanged)
|
||||
EVT_DIRPICKER_CHANGED(ID_TEXTUREPATH, ConfigDialog::TexturePathChange)
|
||||
EVT_RADIOBUTTON(ID_RADIO_COPYEFBTORAM, ConfigDialog::AdvancedSettingsChanged)
|
||||
EVT_RADIOBUTTON(ID_RADIO_COPYEFBTOGL, ConfigDialog::AdvancedSettingsChanged)
|
||||
END_EVENT_TABLE()
|
||||
|
@ -326,11 +326,10 @@ void ConfigDialog::CreateGUIControls()
|
|||
|
||||
// Utility
|
||||
sbUtilities = new wxStaticBoxSizer(wxVERTICAL, m_PageAdvanced, wxT("Utilities"));
|
||||
m_DumpTextures = new wxCheckBox(m_PageAdvanced, ID_DUMPTEXTURES, wxT("Dump textures to:"), wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator);
|
||||
m_DumpTextures = new wxCheckBox(m_PageAdvanced, ID_DUMPTEXTURES, wxT("Dump textures"), wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator);
|
||||
m_DumpTextures->SetValue(g_Config.bDumpTextures);
|
||||
m_TexturePath = new wxDirPickerCtrl(m_PageAdvanced, ID_TEXTUREPATH, wxEmptyString, wxT("Choose a directory to store texture dumps:"), wxDefaultPosition, wxDefaultSize, wxDIRP_USE_TEXTCTRL);
|
||||
m_TexturePath->SetPath(wxString::FromAscii(g_Config.texDumpPath));
|
||||
m_TexturePath->Enable(m_DumpTextures->IsChecked());
|
||||
m_DumpEFBTarget = new wxCheckBox(m_PageAdvanced, ID_DUMPEFBTARGET, wxT("Dump EFB Target"), wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator);
|
||||
m_DumpEFBTarget->SetValue(g_Config.bDumpEFBTarget);
|
||||
|
||||
// Hacks controls
|
||||
m_SafeTextureCache = new wxCheckBox(m_PageAdvanced, ID_SAFETEXTURECACHE, wxT("Use Safe texture cache"), wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator);
|
||||
|
@ -387,7 +386,7 @@ void ConfigDialog::CreateGUIControls()
|
|||
|
||||
sUtilities = new wxBoxSizer(wxHORIZONTAL);
|
||||
sUtilities->Add(m_DumpTextures, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);
|
||||
sUtilities->Add(m_TexturePath, 1, wxALL|wxEXPAND, 5);
|
||||
sUtilities->Add(m_DumpEFBTarget, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);
|
||||
sbUtilities->Add(sUtilities, 1, wxEXPAND);
|
||||
|
||||
// Sizers
|
||||
|
@ -507,9 +506,11 @@ void ConfigDialog::AdvancedSettingsChanged(wxCommandEvent& event)
|
|||
g_Config.bDisableTexturing = m_DisableTexturing->IsChecked();
|
||||
break;
|
||||
case ID_DUMPTEXTURES:
|
||||
m_TexturePath->Enable(m_DumpTextures->IsChecked());
|
||||
g_Config.bDumpTextures = m_DumpTextures->IsChecked();
|
||||
break;
|
||||
case ID_DUMPEFBTARGET:
|
||||
g_Config.bDumpEFBTarget = m_DumpEFBTarget->IsChecked();
|
||||
break;
|
||||
case ID_TEXTUREPATH:
|
||||
break;
|
||||
case ID_CHECKBOX_DISABLECOPYEFB:
|
||||
|
@ -526,7 +527,6 @@ void ConfigDialog::AdvancedSettingsChanged(wxCommandEvent& event)
|
|||
g_Config.bSafeTextureCache = m_SafeTextureCache->IsChecked();
|
||||
break;
|
||||
|
||||
// External frame buffer
|
||||
case ID_RADIO_COPYEFBTORAM:
|
||||
TextureMngr::ClearRenderTargets();
|
||||
g_Config.bCopyEFBToRAM = true;
|
||||
|
@ -549,12 +549,6 @@ void ConfigDialog::AdvancedSettingsChanged(wxCommandEvent& event)
|
|||
UpdateGUI();
|
||||
}
|
||||
|
||||
void ConfigDialog::TexturePathChange(wxFileDirPickerEvent& event)
|
||||
{
|
||||
// Note: if a user inputs an incorrect path(by typing, not by choosing from
|
||||
// the combobox) this event wil not be fired.
|
||||
strcpy(g_Config.texDumpPath, event.GetPath().mb_str());
|
||||
}
|
||||
|
||||
void ConfigDialog::UpdateGUI()
|
||||
{
|
||||
|
|
|
@ -103,10 +103,10 @@ class ConfigDialog : public wxDialog
|
|||
wxCheckBox *m_DisableLighting;
|
||||
wxCheckBox *m_DisableTexturing;
|
||||
wxCheckBox *m_DumpTextures;
|
||||
wxCheckBox *m_DumpEFBTarget;
|
||||
wxStaticBox * m_StaticBox_EFB;
|
||||
wxCheckBox *m_CheckBox_DisableCopyEFB;
|
||||
wxRadioButton *m_Radio_CopyEFBToRAM, *m_Radio_CopyEFBToGL;
|
||||
wxDirPickerCtrl *m_TexturePath;
|
||||
wxCheckBox *m_EFBCopyDisableHotKey;
|
||||
wxCheckBox *m_ProjectionHax1;
|
||||
wxCheckBox *m_SafeTextureCache;
|
||||
|
@ -158,6 +158,7 @@ class ConfigDialog : public wxDialog
|
|||
ID_SAFETEXTURECACHE,
|
||||
|
||||
ID_DUMPTEXTURES,
|
||||
ID_DUMPEFBTARGET,
|
||||
ID_TEXTUREPATH,
|
||||
|
||||
ID_CHECKBOX_DISABLECOPYEFB,
|
||||
|
@ -173,7 +174,6 @@ class ConfigDialog : public wxDialog
|
|||
void AboutClick(wxCommandEvent& event);
|
||||
void GeneralSettingsChanged(wxCommandEvent& event);
|
||||
void AdvancedSettingsChanged(wxCommandEvent& event);
|
||||
void TexturePathChange(wxFileDirPickerEvent& event);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
#include <vector>
|
||||
|
||||
#include "Globals.h"
|
||||
#include "CommonPaths.h"
|
||||
#include "StringUtil.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#define _interlockedbittestandset workaround_ms_header_bug_platform_sdk6_set
|
||||
|
@ -77,16 +79,17 @@ const GLint c_WrapSettings[4] = {
|
|||
|
||||
bool SaveTexture(const char* filename, u32 textarget, u32 tex, int width, int height)
|
||||
{
|
||||
GL_REPORT_ERRORD();
|
||||
std::vector<u32> data(width * height);
|
||||
glBindTexture(textarget, tex);
|
||||
glGetTexImage(textarget, 0, GL_BGRA, GL_UNSIGNED_BYTE, &data[0]);
|
||||
glGetTexImage(textarget, 0, GL_BGRA, GL_UNSIGNED_INT, &data[0]);
|
||||
GLenum err;
|
||||
GL_REPORT_ERROR();
|
||||
if (err != GL_NO_ERROR)
|
||||
{
|
||||
PanicAlert("Can't save texture, GL Error: %s", gluErrorString(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
return SaveTGA(filename, width, height, &data[0]);
|
||||
}
|
||||
|
||||
|
@ -401,8 +404,7 @@ TextureMngr::TCacheEntry* TextureMngr::Load(int texstage, u32 address, int width
|
|||
{
|
||||
static int counter = 0;
|
||||
char szTemp[MAX_PATH];
|
||||
sprintf(szTemp, "%s/txt_%04i_%i.tga", g_Config.texDumpPath, counter++, format);
|
||||
|
||||
sprintf(szTemp, "%s/txt_%04i_%i.tga", FULL_DUMP_TEXTURES_DIR, counter++, format);
|
||||
SaveTexture(szTemp,target, entry.texture, width, height);
|
||||
}
|
||||
|
||||
|
@ -442,17 +444,20 @@ void TextureMngr::CopyRenderTargetToTexture(u32 address, bool bFromZBuffer, bool
|
|||
|
||||
GL_REPORT_ERRORD();
|
||||
|
||||
if (!bIsInit) {
|
||||
if (!bIsInit)
|
||||
{
|
||||
glGenTextures(1, (GLuint *)&entry.texture);
|
||||
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, entry.texture);
|
||||
glTexImage2D(GL_TEXTURE_RECTANGLE_ARB, 0, 4, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||
GL_REPORT_ERRORD();
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
_assert_(entry.texture);
|
||||
bool bReInit = true;
|
||||
|
||||
if (entry.w == w && entry.h == h) {
|
||||
if (entry.w == w && entry.h == h)
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, entry.texture);
|
||||
// for some reason mario sunshine errors here...
|
||||
GLenum err = GL_NO_ERROR;
|
||||
|
@ -461,7 +466,8 @@ void TextureMngr::CopyRenderTargetToTexture(u32 address, bool bFromZBuffer, bool
|
|||
bReInit = false;
|
||||
}
|
||||
|
||||
if (bReInit) {
|
||||
if (bReInit)
|
||||
{
|
||||
// necessary, for some reason opengl gives errors when texture isn't deleted
|
||||
glDeleteTextures(1,(GLuint *)&entry.texture);
|
||||
glGenTextures(1, (GLuint *)&entry.texture);
|
||||
|
@ -494,8 +500,10 @@ void TextureMngr::CopyRenderTargetToTexture(u32 address, bool bFromZBuffer, bool
|
|||
float fConstAdd[4] = {0};
|
||||
memset(colmat, 0, sizeof(colmat));
|
||||
|
||||
if (bFromZBuffer) {
|
||||
switch(copyfmt) {
|
||||
if (bFromZBuffer)
|
||||
{
|
||||
switch(copyfmt)
|
||||
{
|
||||
case 0: // Z4
|
||||
case 1: // Z8
|
||||
colmat[2] = colmat[6] = colmat[10] = colmat[14] = 1;
|
||||
|
@ -525,9 +533,11 @@ void TextureMngr::CopyRenderTargetToTexture(u32 address, bool bFromZBuffer, bool
|
|||
break;
|
||||
}
|
||||
}
|
||||
else if (bIsIntensityFmt) {
|
||||
else if (bIsIntensityFmt)
|
||||
{
|
||||
fConstAdd[0] = fConstAdd[1] = fConstAdd[2] = 16.0f/255.0f;
|
||||
switch (copyfmt) {
|
||||
switch (copyfmt)
|
||||
{
|
||||
case 0: // I4
|
||||
case 1: // I8
|
||||
case 2: // IA4
|
||||
|
@ -536,13 +546,15 @@ void TextureMngr::CopyRenderTargetToTexture(u32 address, bool bFromZBuffer, bool
|
|||
colmat[0] = 0.257f; colmat[1] = 0.504f; colmat[2] = 0.098f;
|
||||
colmat[4] = 0.257f; colmat[5] = 0.504f; colmat[6] = 0.098f;
|
||||
colmat[8] = 0.257f; colmat[9] = 0.504f; colmat[10] = 0.098f;
|
||||
if (copyfmt < 2) {
|
||||
|
||||
if (copyfmt < 2)
|
||||
{
|
||||
fConstAdd[3] = 16.0f / 255.0f;
|
||||
colmat[12] = 0.257f; colmat[13] = 0.504f; colmat[14] = 0.098f;
|
||||
}
|
||||
else { // alpha
|
||||
else// alpha
|
||||
colmat[15] = 1;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
ERROR_LOG(VIDEO, "Unknown copy intensity format: 0x%x\n", copyfmt);
|
||||
|
@ -550,8 +562,10 @@ void TextureMngr::CopyRenderTargetToTexture(u32 address, bool bFromZBuffer, bool
|
|||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch (copyfmt) {
|
||||
else
|
||||
{
|
||||
switch (copyfmt)
|
||||
{
|
||||
case 0: // R4
|
||||
case 8: // R8
|
||||
colmat[0] = colmat[4] = colmat[8] = colmat[12] = 1;
|
||||
|
@ -620,17 +634,21 @@ void TextureMngr::CopyRenderTargetToTexture(u32 address, bool bFromZBuffer, bool
|
|||
// create and attach the render target
|
||||
std::map<u32, DEPTHTARGET>::iterator itdepth = mapDepthTargets.find((h << 16) | w);
|
||||
|
||||
if (itdepth == mapDepthTargets.end()) {
|
||||
if (itdepth == mapDepthTargets.end())
|
||||
{
|
||||
DEPTHTARGET& depth = mapDepthTargets[(h << 16) | w];
|
||||
depth.framecount = frameCount;
|
||||
|
||||
glGenRenderbuffersEXT(1, &depth.targ);
|
||||
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depth.targ);
|
||||
glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT/*GL_DEPTH24_STENCIL8_EXT*/, w, h);
|
||||
glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, w, h);
|
||||
GL_REPORT_ERRORD();
|
||||
|
||||
Renderer::SetDepthTarget(depth.targ);
|
||||
GL_REPORT_ERRORD();
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
itdepth->second.framecount = frameCount;
|
||||
Renderer::SetDepthTarget(itdepth->second.targ);
|
||||
GL_REPORT_ERRORD();
|
||||
|
@ -638,11 +656,9 @@ void TextureMngr::CopyRenderTargetToTexture(u32 address, bool bFromZBuffer, bool
|
|||
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
if (bFromZBuffer) {
|
||||
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, Renderer::ResolveAndGetFakeZTarget(source_rect));
|
||||
} else {
|
||||
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, Renderer::ResolveAndGetRenderTarget(source_rect));
|
||||
}
|
||||
|
||||
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, (bFromZBuffer ? Renderer::ResolveAndGetFakeZTarget(source_rect) : Renderer::ResolveAndGetRenderTarget(source_rect)));
|
||||
|
||||
TextureMngr::EnableTexRECT(0);
|
||||
|
||||
glViewport(0, 0, w, h);
|
||||
|
@ -652,9 +668,11 @@ void TextureMngr::CopyRenderTargetToTexture(u32 address, bool bFromZBuffer, bool
|
|||
PixelShaderManager::SetColorMatrix(colmat, fConstAdd); // set transformation
|
||||
GL_REPORT_ERRORD();
|
||||
|
||||
glBegin(GL_QUADS);
|
||||
// Get Target X, Y
|
||||
float MValueX = Renderer::GetTargetScaleX();
|
||||
float MValueY = Renderer::GetTargetScaleY();
|
||||
|
||||
glBegin(GL_QUADS);
|
||||
glTexCoord2f((float)source_rect.left * MValueX, Renderer::GetTargetHeight()-(float)source_rect.bottom * MValueY); glVertex2f(-1, 1);
|
||||
glTexCoord2f((float)source_rect.left * MValueX, Renderer::GetTargetHeight()-(float)source_rect.top * MValueY); glVertex2f(-1, -1);
|
||||
glTexCoord2f((float)source_rect.right * MValueX, Renderer::GetTargetHeight()-(float)source_rect.top * MValueY); glVertex2f( 1, -1);
|
||||
|
@ -666,15 +684,21 @@ void TextureMngr::CopyRenderTargetToTexture(u32 address, bool bFromZBuffer, bool
|
|||
Renderer::SetFramebuffer(0);
|
||||
Renderer::RestoreGLState();
|
||||
VertexShaderManager::SetViewportChanged();
|
||||
|
||||
TextureMngr::DisableStage(0);
|
||||
|
||||
if (bFromZBuffer)
|
||||
Renderer::SetZBufferRender(); // notify for future settings
|
||||
|
||||
GL_REPORT_ERRORD();
|
||||
//SaveTexture("frame.tga", GL_TEXTURE_RECTANGLE_ARB, entry.texture, entry.w, entry.h);
|
||||
//SaveTexture("tex.tga", GL_TEXTURE_RECTANGLE_ARB, bFromZBuffer?Renderer::GetFakeZTarget():Renderer::GetRenderTarget(), Renderer::GetTargetWidth(), Renderer::GetTargetHeight());
|
||||
|
||||
if(g_Config.bDumpEFBTarget)
|
||||
{
|
||||
SaveTexture(StringFromFormat("%s/efb_frame.tga", FULL_DUMP_TEXTURES_DIR).c_str(), GL_TEXTURE_RECTANGLE_ARB, entry.texture, entry.w, entry.h);
|
||||
//TODO: Fix this
|
||||
//SaveTexture(StringFromFormat("%s/efb_tex.tga", FULL_DUMP_TEXTURES_DIR).c_str(), GL_TEXTURE_RECTANGLE_ARB,
|
||||
// bFromZBuffer ? Renderer::ResolveAndGetFakeZTarget(source_rect) : Renderer::ResolveAndGetRenderTarget(source_rect),
|
||||
// Renderer::GetTargetWidth(), Renderer::GetTargetHeight());
|
||||
}
|
||||
}
|
||||
|
||||
void TextureMngr::EnableTex2D(int stage)
|
||||
|
|
Loading…
Reference in New Issue