From e4455a8e8fe57d1a7caae028cad03b80229f77d0 Mon Sep 17 00:00:00 2001 From: vladislavbelov Date: Thu, 30 Dec 2021 16:24:07 +0000 Subject: [PATCH] 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. --- source/graphics/TerrainTextureEntry.cpp | 3 + source/graphics/TerrainTextureEntry.h | 7 + .../AtlasUI/ScenarioEditor/ScenarioEditor.cpp | 4 + .../AtlasUI/ScenarioEditor/SectionLayout.cpp | 13 +- .../AtlasUI/ScenarioEditor/SectionLayout.h | 3 +- .../ScenarioEditor/Sections/Common/Sidebar.h | 4 +- .../Sections/Terrain/Terrain.cpp | 146 ++++++++++++--- .../ScenarioEditor/Sections/Terrain/Terrain.h | 6 +- .../Handlers/TerrainHandlers.cpp | 172 ++++++++++-------- source/tools/atlas/GameInterface/Messages.h | 7 +- 10 files changed, 259 insertions(+), 106 deletions(-) diff --git a/source/graphics/TerrainTextureEntry.cpp b/source/graphics/TerrainTextureEntry.cpp index c88d05ae16..d84e28a4fc 100644 --- a/source/graphics/TerrainTextureEntry.cpp +++ b/source/graphics/TerrainTextureEntry.cpp @@ -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; } } diff --git a/source/graphics/TerrainTextureEntry.h b/source/graphics/TerrainTextureEntry.h index 1623156624..2b74204569 100644 --- a/source/graphics/TerrainTextureEntry.h +++ b/source/graphics/TerrainTextureEntry.h @@ -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; diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp b/source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp index b25bf7ff2b..6010fd4ef7 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp @@ -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(); diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/SectionLayout.cpp b/source/tools/atlas/AtlasUI/ScenarioEditor/SectionLayout.cpp index a7d4ccbe05..26e7002d42 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/SectionLayout.cpp +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/SectionLayout.cpp @@ -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(); +} diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/SectionLayout.h b/source/tools/atlas/AtlasUI/ScenarioEditor/SectionLayout.h index 80d709044a..85d3b79872 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/SectionLayout.h +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/SectionLayout.h @@ -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; diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Common/Sidebar.h b/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Common/Sidebar.h index 95216ad624..3aa36310d1 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Common/Sidebar.h +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Common/Sidebar.h @@ -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; diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.cpp b/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.cpp index 4ff3b47d9d..bc0499aab9 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.cpp +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.cpp @@ -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 +#include #include #include #include @@ -33,6 +35,15 @@ #include #include +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(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 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*, 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(malloc(preview.imageData.GetSize())); + // imagedata.GetBuffer() gives a Shareable*, 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 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 m_Textures; + struct PreviewButton + { + wxBitmapButton* button; + bool loaded; + }; + std::unordered_map m_PreviewButtons; + DECLARE_EVENT_TABLE(); }; @@ -466,6 +545,12 @@ public: } } + void OnShutdown() + { + for (size_t index = 0; index < GetPageCount(); ++index) + static_cast(GetPage(index))->OnShutdown(); + } + protected: void OnPageChanged(wxNotebookEvent& event) { @@ -502,3 +587,8 @@ void TerrainBottomBar::LoadTerrain() { m_Textures->LoadTerrain(); } + +void TerrainBottomBar::OnShutdown() +{ + m_Textures->OnShutdown(); +} diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.h b/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.h index 34862f9d62..cec418eb3e 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.h +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.h @@ -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); diff --git a/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp b/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp index 9dfe70482e..15c6589e31 100644 --- a/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp +++ b/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp @@ -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 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 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 names; -static sTerrainTexturePreview GetPreview(CTerrainTextureEntry* tex, int width, int height) -{ - sTerrainTexturePreview preview; - preview.name = tex->GetTag().FromUTF8(); - - std::vector 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::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 { diff --git a/source/tools/atlas/GameInterface/Messages.h b/source/tools/atlas/GameInterface/Messages.h index b82f30bf3a..9005d01849 100644 --- a/source/tools/atlas/GameInterface/Messages.h +++ b/source/tools/atlas/GameInterface/Messages.h @@ -315,6 +315,11 @@ QUERY(GetTerrainGroups, ((std::vector, groupNames)) ); +QUERY(GetTerrainGroupTextures, + ((std::wstring, groupName)), + ((std::vector, names)) + ); + #ifndef MESSAGES_SKIP_STRUCTS struct sTerrainTexturePreview { @@ -322,7 +327,7 @@ struct sTerrainTexturePreview Shareable loaded; Shareable imageWidth; Shareable imageHeight; - Shareable > imageData; // RGB*width*height + Shareable> imageData; // RGB*width*height }; SHAREABLE_STRUCT(sTerrainTexturePreview); #endif