Speedups terrain painting tab in Atlas by asynchronous texture loading.

Tested By: Silier, Stan
Differential Revision: https://code.wildfiregames.com/D4405
This was SVN commit r26142.
This commit is contained in:
Vladislav Belov 2021-12-30 16:24:07 +00:00
parent ae32055c9b
commit e4455a8e8f
10 changed files with 259 additions and 106 deletions

View File

@ -30,6 +30,7 @@
#include "lib/tex/tex.h"
#include "lib/utf8.h"
#include "ps/CLogger.h"
#include "ps/CStrInternStatic.h"
#include "ps/Filesystem.h"
#include "ps/XML/Xeromyces.h"
#include "renderer/Renderer.h"
@ -96,6 +97,8 @@ CTerrainTextureEntry::CTerrainTextureEntry(CTerrainPropertiesPtr properties, con
name = relativePath.Value;
}
samplers.emplace_back(name, terrainTexturePath);
if (name == str_baseTex.string())
m_DiffuseTexturePath = terrainTexturePath;
}
}

View File

@ -53,6 +53,11 @@ public:
// mapping world-space (x,y,z,1) coordinates onto (u,v,0,1) texcoords
const float* GetTextureMatrix() const;
// Used in Atlas to retrieve a texture for previews. Can't use textures
// directly because they're required on CPU side. Another solution is to
// retrieve path from diffuse texture from material.
const VfsPath& GetDiffuseTexturePath() const { return m_DiffuseTexturePath; }
// Get mipmap color in BGRA format
u32 GetBaseColor()
{
@ -66,6 +71,8 @@ private:
// Tag = file name stripped of path and extension (grass_dark_1)
CStr m_Tag;
VfsPath m_DiffuseTexturePath;
// The property sheet used by this texture
CTerrainPropertiesPtr m_pProperties;

View File

@ -663,6 +663,10 @@ void ScenarioEditor::OnClose(wxCloseEvent& event)
m_FileHistory.SaveToSubDir(*wxConfigBase::Get());
// We notify all clients that might interact with the game after its
// shutdown to prevent accessing invalid state.
m_SectionLayout.OnShutdown();
POST_MESSAGE(Shutdown, ());
qExit().Post();

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -184,6 +184,12 @@ public:
m_Pages[i].bar->OnMapReload();
}
void OnShutdown()
{
for (size_t i = 0; i < m_Pages.size(); ++i)
m_Pages[i].bar->OnShutdown();
}
protected:
void OnPageChanged(SidebarPage oldPage, SidebarPage newPage)
@ -312,3 +318,8 @@ void SectionLayout::OnMapReload()
{
m_SidebarBook->OnMapReload();
}
void SectionLayout::OnShutdown()
{
m_SidebarBook->OnShutdown();
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -52,6 +52,7 @@ public:
void SelectPage(const wxString& classname);
void OnMapReload();
void OnShutdown();
private:
SidebarBook* m_SidebarBook;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2009 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -32,6 +32,8 @@ public:
virtual void OnMapReload() {}
virtual void OnShutdown() {}
protected:
ScenarioEditor& m_ScenarioEditor;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -26,6 +26,8 @@
#include "GameInterface/Messages.h"
#include <chrono>
#include <unordered_map>
#include <wx/spinctrl.h>
#include <wx/listctrl.h>
#include <wx/image.h>
@ -33,6 +35,15 @@
#include <wx/busyinfo.h>
#include <wx/notebook.h>
namespace
{
const int PREVIEW_RELOAD_DELAY_MILLISECONDS = 2000;
const int PREVIEW_RELOAD_TIMEOUT_DELAY_MILLISECONDS = 200;
const float PREVIEW_RELOAD_TIMEOUT_THRESHOLD_SECONDS = 0.1f;
} // anonymous namespace
class TextureNotebook;
class TerrainBottomBar : public wxPanel
@ -40,6 +51,7 @@ class TerrainBottomBar : public wxPanel
public:
TerrainBottomBar(ScenarioEditor& scenarioEditor, wxWindow* parent);
void LoadTerrain();
void OnShutdown();
private:
TextureNotebook* m_Textures;
};
@ -130,7 +142,7 @@ public:
}
else if (!preview.loaded && !m_Timer.IsRunning())
{
m_Timer.Start(2000);
m_Timer.Start(PREVIEW_RELOAD_DELAY_MILLISECONDS);
}
}
@ -249,6 +261,11 @@ TerrainSidebar::TerrainSidebar(ScenarioEditor& scenarioEditor, wxWindow* sidebar
m_BottomBar = new TerrainBottomBar(scenarioEditor, bottomBarContainer);
}
void TerrainSidebar::OnShutdown()
{
static_cast<TerrainBottomBar*>(m_BottomBar)->OnShutdown();
}
void TerrainSidebar::OnFirstDisplay()
{
AtlasMessage::qGetTerrainPassabilityClasses qry;
@ -314,66 +331,114 @@ public:
wxBusyInfo busy (_("Loading terrain previews"));
AtlasMessage::qGetTerrainGroupTextures query((std::wstring)m_Name.wc_str());
query.Post();
m_Textures = *query.names;
LayoutButtons();
ReloadPreviews();
}
void ReloadPreviews()
void LayoutButtons()
{
Freeze();
m_ScrolledPanel->DestroyChildren();
m_ItemSizer->Clear();
m_LastTerrainSelection = NULL; // clear any reference to deleted button
m_LastTerrainSelection = nullptr; // clear any reference to deleted button
AtlasMessage::qGetTerrainGroupPreviews qry((std::wstring)m_Name.wc_str(), imageWidth, imageHeight);
qry.Post();
std::vector<AtlasMessage::sTerrainTexturePreview> previews = *qry.previews;
bool allLoaded = true;
for (size_t i = 0; i < previews.size(); ++i)
for (const std::wstring& textureName : m_Textures)
{
if (!previews[i].loaded)
allLoaded = false;
wxString name = previews[i].name.c_str();
// Construct the wrapped-text label
wxStaticText* label = new wxStaticText(m_ScrolledPanel, wxID_ANY, FormatTextureName(name), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER);
wxStaticText* label = new wxStaticText(m_ScrolledPanel, wxID_ANY, FormatTextureName(textureName), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER);
label->Wrap(imageWidth);
unsigned char* buf = (unsigned char*)(malloc(previews[i].imageData.GetSize()));
// imagedata.GetBuffer() gives a Shareable<unsigned char>*, which
// is stored the same as a unsigned char*, so we can just copy it.
memcpy(buf, previews[i].imageData.GetBuffer(), previews[i].imageData.GetSize());
wxImage img (imageWidth, imageHeight, buf);
wxImage image(imageWidth, imageHeight);
wxBitmapButton* button = new wxBitmapButton(m_ScrolledPanel, wxID_ANY, wxBitmap(image));
wxButton* button = new wxBitmapButton(m_ScrolledPanel, wxID_ANY, wxBitmap(img));
// Store the texture name in the clientdata slot
button->SetClientObject(new wxStringClientData(name));
button->SetClientObject(new wxStringClientData(textureName));
wxSizer* imageSizer = new wxBoxSizer(wxVERTICAL);
imageSizer->Add(button, wxSizerFlags().Center());
imageSizer->Add(label, wxSizerFlags().Proportion(1).Center());
m_ItemSizer->Add(imageSizer, wxSizerFlags().Expand());
m_PreviewButtons.emplace(textureName, PreviewButton{button, false});
}
m_ScrolledPanel->Fit();
Layout();
Thaw();
}
void ReloadPreviews()
{
bool allLoaded = true;
bool timeout = false;
const std::chrono::high_resolution_clock::time_point reloadingStart =
std::chrono::high_resolution_clock::now();
for (const std::wstring& textureName : m_Textures)
{
const auto it = m_PreviewButtons.find(textureName);
if (it == m_PreviewButtons.end() || it->second.loaded)
continue;
if (timeout)
{
// Mark allLoaded only in case we have a real not loaded texture, and not
// because we have an exceeded timeout.
allLoaded = false;
continue;
}
AtlasMessage::qGetTerrainTexturePreview previewQuery(textureName, imageWidth, imageHeight);
previewQuery.Post();
AtlasMessage::sTerrainTexturePreview preview = previewQuery.preview;
if (!preview.loaded)
allLoaded = false;
else
it->second.loaded = true;
if (preview.imageData.GetSize())
{
unsigned char* buffer = reinterpret_cast<unsigned char*>(malloc(preview.imageData.GetSize()));
// imagedata.GetBuffer() gives a Shareable<unsigned char>*, which
// is stored the same as a unsigned char*, so we can just copy it.
memcpy(buffer, preview.imageData.GetBuffer(), preview.imageData.GetSize());
wxImage image(imageWidth, imageHeight, buffer);
it->second.button->SetBitmap(wxBitmap(image));
}
// We need to load at least one preview so check for timeout inside real
// loading.
const std::chrono::high_resolution_clock::time_point now =
std::chrono::high_resolution_clock::now();
const std::chrono::duration<float> delta = now - reloadingStart;
if (delta.count() > PREVIEW_RELOAD_TIMEOUT_THRESHOLD_SECONDS)
timeout = true;
}
// If not all textures were loaded yet, run a timer to reload the previews
// every so often until they've all finished
// every so often until they've all finished.
if (allLoaded && m_Timer.IsRunning())
{
m_Timer.Stop();
m_PreviewButtons.clear();
}
else if (!allLoaded && !m_Timer.IsRunning())
else if (!allLoaded)
{
m_Timer.Start(2000);
if (timeout)
{
// In case we didn't have enough time to load all previews
// start after a minimum delay to not freeze the whole UI.
m_Timer.Start(PREVIEW_RELOAD_TIMEOUT_DELAY_MILLISECONDS);
}
else
m_Timer.Start(PREVIEW_RELOAD_DELAY_MILLISECONDS);
}
}
@ -409,6 +474,12 @@ public:
ReloadPreviews();
}
void OnShutdown()
{
if (m_Timer.IsRunning())
m_Timer.Stop();
}
private:
ScenarioEditor& m_ScenarioEditor;
bool m_Loaded;
@ -418,6 +489,14 @@ private:
wxGridSizer* m_ItemSizer;
wxButton* m_LastTerrainSelection; // button that was last selected, so we can undo its coloring
std::vector<std::wstring> m_Textures;
struct PreviewButton
{
wxBitmapButton* button;
bool loaded;
};
std::unordered_map<std::wstring, PreviewButton> m_PreviewButtons;
DECLARE_EVENT_TABLE();
};
@ -466,6 +545,12 @@ public:
}
}
void OnShutdown()
{
for (size_t index = 0; index < GetPageCount(); ++index)
static_cast<TextureNotebookPage*>(GetPage(index))->OnShutdown();
}
protected:
void OnPageChanged(wxNotebookEvent& event)
{
@ -502,3 +587,8 @@ void TerrainBottomBar::LoadTerrain()
{
m_Textures->LoadTerrain();
}
void TerrainBottomBar::OnShutdown()
{
m_Textures->OnShutdown();
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -24,8 +24,10 @@ class TerrainSidebar : public Sidebar
public:
TerrainSidebar(ScenarioEditor& scenarioEditor, wxWindow* sidebarContainer, wxWindow* bottomBarContainer);
void OnShutdown() override;
protected:
virtual void OnFirstDisplay();
void OnFirstDisplay() override;
private:
void OnPassabilityChoice(wxCommandEvent& evt);

View File

@ -27,7 +27,8 @@
#include "graphics/Terrain.h"
#include "ps/Game.h"
#include "ps/World.h"
#include "lib/ogl.h"
#include "lib/tex/tex.h"
#include "ps/Filesystem.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpPathfinder.h"
#include "simulation2/components/ICmpTerrain.h"
@ -42,6 +43,93 @@
namespace AtlasMessage
{
namespace
{
sTerrainTexturePreview MakeEmptyTerrainTexturePreview()
{
sTerrainTexturePreview preview{};
preview.name = std::wstring();
preview.loaded = false;
preview.imageHeight = 0;
preview.imageWidth = 0;
preview.imageData = {};
return preview;
}
bool CompareTerrain(const sTerrainTexturePreview& a, const sTerrainTexturePreview& b)
{
return (wcscmp(a.name.c_str(), b.name.c_str()) < 0);
}
sTerrainTexturePreview GetPreview(CTerrainTextureEntry* tex, size_t width, size_t height)
{
sTerrainTexturePreview preview;
preview.name = tex->GetTag().FromUTF8();
const size_t previewBPP = 3;
std::vector<u8> buffer(width * height * previewBPP);
// It's not good to shrink the entire texture to fit the small preview
// window, since it's the fine details in the texture that are
// interesting; so just go down one mipmap level, then crop a chunk
// out of the middle.
std::shared_ptr<u8> fileData;
size_t fileSize;
Tex texture;
const bool canUsePreview =
!tex->GetDiffuseTexturePath().empty() &&
g_VFS->LoadFile(tex->GetDiffuseTexturePath(), fileData, fileSize) == INFO::OK &&
texture.decode(fileData, fileSize) == INFO::OK &&
// Check that we can fit the texture into the preview size before any transform.
texture.m_Width >= width && texture.m_Height >= height &&
// Transform to a single format that we can process.
texture.transform_to((texture.m_Flags) & ~(TEX_DXT | TEX_MIPMAPS | TEX_GREY | TEX_BGR)) == INFO::OK &&
(texture.m_Bpp == 24 || texture.m_Bpp == 32);
if (canUsePreview)
{
size_t level = 0;
while ((texture.m_Width >> (level + 1)) >= width && (texture.m_Height >> (level + 1)) >= height)
++level;
// Extract the middle section (as a representative preview),
// and copy into buffer.
u8* data = texture.get_data();
const size_t dataShiftX = ((texture.m_Width - width) / 2) >> level;
const size_t dataShiftY = ((texture.m_Height - height) / 2) >> level;
for (size_t y = 0; y < height; ++y)
for (size_t x = 0; x < width; ++x)
{
const size_t bufferOffset = (y * width + x) * previewBPP;
const size_t dataOffset = (((y << level) + dataShiftY) * texture.m_Width + (x << level) + dataShiftX) * texture.m_Bpp / 8;
buffer[bufferOffset + 0] = data[dataOffset + 0];
buffer[bufferOffset + 1] = data[dataOffset + 1];
buffer[bufferOffset + 2] = data[dataOffset + 2];
}
preview.loaded = true;
}
else
{
// Too small to preview. Just use a flat color instead.
const u32 baseColor = tex->GetBaseColor();
for (size_t i = 0; i < width * height; ++i)
{
buffer[i * previewBPP + 0] = (baseColor >> 16) & 0xff;
buffer[i * previewBPP + 1] = (baseColor >> 8) & 0xff;
buffer[i * previewBPP + 2] = (baseColor >> 0) & 0xff;
}
preview.loaded = tex->GetTexture()->IsLoaded();
}
preview.imageWidth = width;
preview.imageHeight = height;
preview.imageData = buffer;
return preview;
}
} // anonymous namespace
QUERYHANDLER(GetTerrainGroups)
{
const CTerrainTextureManager::TerrainGroupMap &groups = g_TexMan.GetGroups();
@ -51,70 +139,18 @@ QUERYHANDLER(GetTerrainGroups)
msg->groupNames = groupNames;
}
static bool CompareTerrain(const sTerrainTexturePreview& a, const sTerrainTexturePreview& b)
QUERYHANDLER(GetTerrainGroupTextures)
{
return (wcscmp(a.name.c_str(), b.name.c_str()) < 0);
}
std::vector<std::wstring> names;
static sTerrainTexturePreview GetPreview(CTerrainTextureEntry* tex, int width, int height)
{
sTerrainTexturePreview preview;
preview.name = tex->GetTag().FromUTF8();
std::vector<unsigned char> buf (width*height*3);
#if !CONFIG2_GLES
// It's not good to shrink the entire texture to fit the small preview
// window, since it's the fine details in the texture that are
// interesting; so just go down one mipmap level, then crop a chunk
// out of the middle.
// Read the size of the texture. (Usually loads the texture from
// disk, which is slow.)
tex->GetTexture()->Bind();
int level = 1; // level 0 is the original size
int w = std::max(1, (int)tex->GetTexture()->GetWidth() >> level);
int h = std::max(1, (int)tex->GetTexture()->GetHeight() >> level);
if (w >= width && h >= height)
CTerrainGroup* group = g_TexMan.FindGroup(CStrW(*msg->groupName).ToUTF8());
if (group)
{
// Read the whole texture into a new buffer
unsigned char* texdata = new unsigned char[w*h*3];
glGetTexImage(GL_TEXTURE_2D, level, GL_RGB, GL_UNSIGNED_BYTE, texdata);
// Extract the middle section (as a representative preview),
// and copy into buf
unsigned char* texdata_ptr = texdata + (w*(h - height)/2 + (w - width)/2) * 3;
unsigned char* buf_ptr = &buf[0];
for (ssize_t y = 0; y < height; ++y)
{
memcpy(buf_ptr, texdata_ptr, width*3);
buf_ptr += width*3;
texdata_ptr += w*3;
}
delete[] texdata;
for (std::vector<CTerrainTextureEntry*>::const_iterator it = group->GetTerrains().begin(); it != group->GetTerrains().end(); ++it)
names.emplace_back((*it)->GetTag().FromUTF8());
}
else
#endif
{
// Too small to preview, or glGetTexImage not supported (on GLES)
// Just use a flat color instead
u32 c = tex->GetBaseColor();
for (ssize_t i = 0; i < width*height; ++i)
{
buf[i*3+0] = (c>>16) & 0xff;
buf[i*3+1] = (c>>8) & 0xff;
buf[i*3+2] = (c>>0) & 0xff;
}
}
preview.loaded = tex->GetTexture()->IsLoaded();
preview.imageWidth = width;
preview.imageHeight = height;
preview.imageData = buf;
return preview;
std::sort(names.begin(), names.end());
msg->names = names;
}
QUERYHANDLER(GetTerrainGroupPreviews)
@ -170,23 +206,15 @@ QUERYHANDLER(GetTerrainTexturePreview)
{
CTerrainTextureEntry* tex = g_TexMan.FindTexture(CStrW(*msg->name).ToUTF8());
if (tex)
{
msg->preview = GetPreview(tex, msg->imageWidth, msg->imageHeight);
}
else
{
sTerrainTexturePreview noPreview{};
noPreview.name = std::wstring();
noPreview.loaded = false;
noPreview.imageHeight = 0;
noPreview.imageWidth = 0;
msg->preview = noPreview;
}
msg->preview = MakeEmptyTerrainTexturePreview();
}
//////////////////////////////////////////////////////////////////////////
namespace {
namespace
{
struct TerrainTile
{

View File

@ -315,6 +315,11 @@ QUERY(GetTerrainGroups,
((std::vector<std::wstring>, groupNames))
);
QUERY(GetTerrainGroupTextures,
((std::wstring, groupName)),
((std::vector<std::wstring>, names))
);
#ifndef MESSAGES_SKIP_STRUCTS
struct sTerrainTexturePreview
{
@ -322,7 +327,7 @@ struct sTerrainTexturePreview
Shareable<bool> loaded;
Shareable<int> imageWidth;
Shareable<int> imageHeight;
Shareable<std::vector<unsigned char> > imageData; // RGB*width*height
Shareable<std::vector<unsigned char>> imageData; // RGB*width*height
};
SHAREABLE_STRUCT(sTerrainTexturePreview);
#endif