This is the Polish translation of In-Practice/Debugging article of learnopengl.com tutorial series.
Programowanie grafiki może sprawiać wiele radości, ale może być także dużym źródłem frustracji, gdy coś nie renderuje się poprawnie, a nawet nie renderuje się wcale! Ponieważ większość naszych działań polega na manipulowaniu pikselami, może być trudno znaleźć przyczynę błędu, gdy coś nie działa tak, jak powinno. Debugowanie tego rodzaju wizualnych błędów jest inne niż zwykłe debugowanie błędów w normalnych programach. Nie mamy konsoli do wyprowadzania tekstu, żadnych punktów przerwania, które można ustawić w naszym kodzie GLSL, i nie można łatwo sprawdzić stanu wykonania GPU.
W tym samouczku zajmiemy się kilkoma technikami i sztuczkami debugowania twojego programu OpenGL. Debugowanie w OpenGL nie jest zbyt trudne, a zrozumienie tych technik zdecydowanie opłaca się na dłuższą metę.
glGetError()
W momencie, gdy niepoprawnie użyjesz OpenGL (jak konfiguracja bufora bez wcześniejszego powiązania (ang. binding)), zostanie to zauważone i wygenerowana zostanie jedna lub więcej flag błędów użytkownika za kulisami. Możemy zapytać o te flagi błędów za pomocą funkcji o nazwie
GLenum glGetError();
W momencie wywołania funkcji
Flaga | Kod | Opis |
---|---|---|
GL_NO_ERROR | 0 | Nie zgłoszono żadnego błędu użytkownika od ostatniego wywołania |
GL_INVALID_ENUM | 1280 | Ustawiona, gdy parametr wyliczeniowy nie jest poprawny. |
GL_INVALID_VALUE | 1281 | Ustawiona, gdy parametr wartości nie jest poprawny. |
GL_INVALID_OPERATION | 1282 | Ustawiona, gdy stan polecenia nie jest poprawny dla podanych parametrów. |
GL_STACK_OVERFLOW | 1283 | Ustawiona, gdy operacja wypychania stosu powoduje przepełnienie stosu. |
GL_STACK_UNDERFLOW | 1284 | Ustawiona, gdy nastąpi operacja pobierania wartości stosu, gdy stos znajduje się w najniższym punkcie. |
GL_OUT_OF_MEMORY | 1285 | Ustawiona, gdy operacja przydzielania pamięci nie może przydzielić (wystarczającej) ilości pamięci. |
GL_INVALID_FRAMEBUFFER_OPERATION | 1286 | Ustawiana podczas odczytu lub zapisu do bufora ramki, który nie jest kompletny. |
W dokumentacji OpenGL dla danej funkcji zawsze można znaleźć kody błędów, które funkcja generuje w momencie, gdy jest niewłaściwie używana. Na przykład, jeśli przejrzysz dokumentację funkcji glBindTexture, możesz znaleźć (w sekcji Errors) wszystkie kody błędów użytkownika, które można wygenerować.
W momencie ustawienia flagi błędu nie będą zgłaszane żadne inne flagi błędów. Ponadto, w momencie wywołania funkcji
Zauważ, że gdy OpenGL działa w systemach X11, inne kody błędów użytkowników mogą być generowane, o ile mają różne kody błędów. Wywołanie funkcji
glBindTexture(GL_TEXTURE_2D, tex);
std::cout << glGetError() << std::endl; // returns 0 (no error)
glTexImage2D(GL_TEXTURE_3D, 0, GL_RGB, 512, 512, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
std::cout << glGetError() << std::endl; // returns 1280 (invalid enum)
glGenTextures(-5, textures);
std::cout << glGetError() << std::endl; // returns 1281 (invalid value)
std::cout << glGetError() << std::endl; // returns 0 (no error)
Wspaniałą rzeczą w
Domyślnie
GLenum glCheckError_(const char *file, int line)
{
GLenum errorCode;
while ((errorCode = glGetError()) != GL_NO_ERROR)
{
std::string error;
switch (errorCode)
{
case GL_INVALID_ENUM: error = "INVALID_ENUM"; break;
case GL_INVALID_VALUE: error = "INVALID_VALUE"; break;
case GL_INVALID_OPERATION: error = "INVALID_OPERATION"; break;
case GL_STACK_OVERFLOW: error = "STACK_OVERFLOW"; break;
case GL_STACK_UNDERFLOW: error = "STACK_UNDERFLOW"; break;
case GL_OUT_OF_MEMORY: error = "OUT_OF_MEMORY"; break;
case GL_INVALID_FRAMEBUFFER_OPERATION: error = "INVALID_FRAMEBUFFER_OPERATION"; break;
}
std::cout << error << " | " << file << " (" << line << ")" << std::endl;
}
return errorCode;
}
#define glCheckError() glCheckError_(__FILE__, __LINE__)
W przypadku, gdy nie zdajesz sobie sprawy z tego, czym są dyrektywy preprocesora __FILE__
i __LINE__
: zmienne te są zastępowane podczas kompilacji odpowiednią nazwą pliku i linią, w której zostały skompilowane. Jeśli zdecydujemy się na umieszczenie dużej liczby wywołań
glBindBuffer(GL_VERTEX_ARRAY, vbo);
glCheckError();
Da nam to następujące wyniki:
Jedną ważną rzeczą, którą należy wymienić, jest to, że GLEW ma błąd (już długo istniejący), w którym wywołanie
glewInit();
glGetError();
Funkcja
Debug output
Rzadziej używanym, ale bardziej użytecznym narzędziem niż
Debug output jest w core od wersji OpenGL 4.3, co oznacza, że znajdziesz tę funkcjonalność na dowolnym komputerze, na którym działa OpenGL 4.3 lub nowszy. Jeśli nie jest dostępna ta wersja OpenGL, jego funkcjonalność można uzyskać za pomocą rozszerzenia ARB_debug_output
lub AMD_debug_output
. Zauważ, że OS X wydaje się nie obsługiwać funkcjonalności debug output (jak piszą ludzie w Internecie, sam tego nie przetestowałem - daj mi znać, jeśli się mylę).
Aby rozpocząć korzystanie z debug output, musimy zażądać kontekstu debug output OpenGL podczas procesu inicjalizacji. Ten proces różni się w zależności od używanego systemu okienkowego; tutaj omówimy ustawienie go na GLFW, ale na końcu znajdziesz informacje o innych systemach w sekcji Dodatkowe materiały.
Debug output w GLFW
Żądanie kontekstu debugowania w GLFW jest zaskakująco łatwe, ponieważ wszystko, co musimy zrobić, to przekazać GLFW wskazówkę (ang. hint), że chcielibyśmy mieć kontekst debug output. Musimy to zrobić przed wywołaniem
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
Po zainicjowaniu GLFW powinniśmy mieć kontekst debugowania, jeśli używamy OpenGL w wersji 4.3 lub wyższej. W przeciwnym razie musimy poprosić o debug output za pomocą rozszerzeń OpenGL.
Używanie OpenGL w kontekście debugowania może być znacznie wolniejsze w porównaniu do kontekstu bez debugowania, więc podczas pracy nad optymalizacją aplikacji, chcesz usunąć lub za komentować wskazówkę dotyczącą kontekstu debugowania GLFW.
Aby sprawdzić, czy udało nam się zainicjować kontekst debugowania, możemy zapytać o to OpenGL:
GLint flags; glGetIntegerv(GL_CONTEXT_FLAGS, &flags);
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT)
{
// initialize debug output
}
Sposób działania debug output polega na tym, że przekazujemy OpenGL wywołanie zwrotne funkcji (ang. callback) rejestrowania błędów (podobne do wywołań I/O GLFW), a w funkcji wywołania zwrotnego możemy przetwarzać dane o błędach OpenGL zgodnie z naszymi oczekiwaniami; w naszym przypadku będziemy wyświetlać przydatne dane o błędach w konsoli. Poniżej znajduje się prototyp funkcji wywołania zwrotnego, którego OpenGL oczekuje dla debug output:
void APIENTRY glDebugOutput(GLenum source, GLenum type, GLuint id, GLenum severity,
GLsizei length, const GLchar *message, void *userParam);
Zauważ, że w niektórych implementacjach OpenGL oczekuje, że ostatni parametr będzie typu const void*
zamiast void*
.
Biorąc pod uwagę duży zestaw danych, które mamy do użycia, możemy stworzyć przydatne narzędzie do wypisywania błędów, takie jak poniżej:
void APIENTRY glDebugOutput(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar *message,
void *userParam)
{
// ignore non-significant error/warning codes
if(id == 131169 || id == 131185 || id == 131218 || id == 131204) return;
std::cout << "---------------" << std::endl;
std::cout << "Debug message (" << id << "): " << message << std::endl;
switch (source)
{
case GL_DEBUG_SOURCE_API: std::cout << "Source: API"; break;
case GL_DEBUG_SOURCE_WINDOW_SYSTEM: std::cout << "Source: Window System"; break;
case GL_DEBUG_SOURCE_SHADER_COMPILER: std::cout << "Source: Shader Compiler"; break;
case GL_DEBUG_SOURCE_THIRD_PARTY: std::cout << "Source: Third Party"; break;
case GL_DEBUG_SOURCE_APPLICATION: std::cout << "Source: Application"; break;
case GL_DEBUG_SOURCE_OTHER: std::cout << "Source: Other"; break;
} std::cout << std::endl;
switch (type)
{
case GL_DEBUG_TYPE_ERROR: std::cout << "Type: Error"; break;
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: std::cout << "Type: Deprecated Behaviour"; break;
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: std::cout << "Type: Undefined Behaviour"; break;
case GL_DEBUG_TYPE_PORTABILITY: std::cout << "Type: Portability"; break;
case GL_DEBUG_TYPE_PERFORMANCE: std::cout << "Type: Performance"; break;
case GL_DEBUG_TYPE_MARKER: std::cout << "Type: Marker"; break;
case GL_DEBUG_TYPE_PUSH_GROUP: std::cout << "Type: Push Group"; break;
case GL_DEBUG_TYPE_POP_GROUP: std::cout << "Type: Pop Group"; break;
case GL_DEBUG_TYPE_OTHER: std::cout << "Type: Other"; break;
} std::cout << std::endl;
switch (severity)
{
case GL_DEBUG_SEVERITY_HIGH: std::cout << "Severity: high"; break;
case GL_DEBUG_SEVERITY_MEDIUM: std::cout << "Severity: medium"; break;
case GL_DEBUG_SEVERITY_LOW: std::cout << "Severity: low"; break;
case GL_DEBUG_SEVERITY_NOTIFICATION: std::cout << "Severity: notification"; break;
} std::cout << std::endl;
std::cout << std::endl;
}
Kiedy debug output wykryje błąd OpenGL, wywołaja tę funkcję zwrotną i będziemy mogli wypisać dużą ilość informacji dotyczących błędu OpenGL. Zauważ, że zignorowaliśmy kilka kodów błędów, które zwykle nie wyświetlają niczego użytecznego (np. 131185
w sterownikach NVidia, które mówią nam, że bufor został pomyślnie utworzony).
Teraz, gdy mamy funkcję wywołania zwrotnego, nadszedł czas na zainicjowanie debug output:
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT)
{
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
glDebugMessageCallback(glDebugOutput, nullptr);
glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE);
}
Tutaj mówimy OpenGL, aby włączył debug output. Wywołanie
Filtrowanie debug output
Dzięki
glDebugMessageControl(GL_DEBUG_SOURCE_API,
GL_DEBUG_TYPE_ERROR,
GL_DEBUG_SEVERITY_HIGH,
0, nullptr, GL_TRUE);
Biorąc pod uwagę naszą konfigurację i zakładając, że masz kontekst obsługujący dane wyjściowe debugowania, każde niepoprawne polecenie OpenGL wydrukuje teraz duży pakiet przydatnych danych:
Śledzenie źródła błędu
Kolejną świetną sztuczką z debug output jest to, że możesz stosunkowo łatwo pobrać dokładny numer linii lub wywołać błąd. Ustawiając punkt przerwania (ang. breakpoint) w
Wymaga to ręcznej interwencji, ale jeśli z grubsza wiesz, czego szukasz, niezwykle przydatne jest szybkie ustalenie, które wywołanie powoduje błąd.
Niestandardowe wyjście błędów
Oprócz odczytywania wiadomości, możemy również przekazać wiadomości do systemu debug output za pomocą
glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_ERROR, 0,
GL_DEBUG_SEVERITY_MEDIUM, -1, "error message here");
Jest to szczególnie przydatne, jeśli podłączasz się do innej aplikacji lub kodu OpenGL, który wykorzystuje kontekst debug output. Inni programiści mogą szybko wykryć każdy zgłoszony błąd występujący w twoim niestandardowym kodzie OpenGL.
Podsumowując, debug output (jeśli można z niego korzystać) jest niezwykle przydatny do szybkiego wychwytywania błędów i jest wart wysiłku jeżeli chodzi o konfigurację, ponieważ oszczędza to czas podczas pisania programu. Możesz znaleźć kopię kodu źródłowego tutaj zarówno z
Debugowanie wyjścia shaderów
Jeśli chodzi o GLSL, niestety nie mamy dostępu do funkcji takich jak
Często używaną sztuczką, aby dowiedzieć się, co jest nie tak z shaderem, jest ocena wszystkich istotnych zmiennych w programie cieniującym poprzez wysłanie ich bezpośrednio do kanału wyjściowego Fragment Shadera. Poprzez wysyłanie zmiennych shadera bezpośrednio do wyjściowych kanałów kolorów, często możemy przekazać interesujące informacje, sprawdzając wyniki wizualne. Na przykład, powiedzmy, że chcemy sprawdzić, czy model ma poprawne wektory normalne, możemy przekazać je (przekształcone lub nie) z Vertex Shadera do Fragment Shadera, w którym następnie wyprowadzilibyśmy normalne w następujący sposób:
#version 330 core
out vec4 FragColor;
in vec3 Normal;
[...]
void main()
{
[...]
FragColor.rgb = Normal;
FragColor.a = 1.0f;
}
Wyprowadzając zmienną (nie opisującą koloru) do wyjściowego kanału koloru, możemy szybko sprawdzić, czy zmienna wyświetla prawidłowe wartości. Jeśli na przykład wynik wizualny jest całkowicie czarny, jasne jest, że wektory normalne nie są poprawnie przekazywane do shaderów; a kiedy są wyświetlane, względnie łatwo sprawdzić, czy są poprawne, czy nie:
Z wyników wizualnych widzimy, że wektory normalne wydają się być poprawne, ponieważ prawa strona modelu nanokombinezonu ma głównie kolor czerwony (co oznaczałoby, że normalne są skierowane z grubsza (poprawnie) w kierunku dodatniej osi x) i podobnie przednia strona nanokombinezon jest zabarwiona w kierunku dodatniej osi z (niebieski).
Takie podejście można łatwo rozszerzyć na dowolny typ zmiennej, którą chcesz przetestować. Za każdym razem, gdy utkniesz i podejrzewasz, że coś jest nie tak z twoimi shaderami, spróbuj wyświetlić wiele zmiennych i/lub wyników pośrednich, aby zobaczyć, w której części algorytmu coś jest niepoprawne.
Kompilator referencyjny GLSL OpenGL
Każdy sterownik ma swoje własne dziwactwa i ciekawostki; na przykład sterowniki NVIDIA są mniej restrykcyjne i przeoczają pewne ograniczenia specyfikacji, podczas gdy sterowniki ATI/AMD mają tendencję do lepszego egzekwowania specyfikacji OpenGL (co jest moim zdaniem lepszym rozwiązaniem). Problem polega na tym, że shadery na jednym komputerze mogą nie działać z powodu różnic między sterownikami.
Dzięki kilkunastoletniemu doświadczeniu w końcu poznasz drobne różnice między dostawcami GPU, ale jeśli chcesz mieć pewność, że twój kod shadera działa na wszystkich rodzajach maszyn, możesz bezpośrednio sprawdzić swój kod shadera względem oficjalnej specyfikacji używając referencyjnego kompilatora GLSL OpenGL. Możesz pobrać tak zwane pliki binarne
Biorąc pod uwagę binarny walidator języka GLSL, możesz łatwo sprawdzić kod shadera, przekazując go jako pierwszy argument. Należy pamiętać, że walidator GLSL lang określa typ shadera według listy stałych rozszerzeń:
.vert
: vertex shader..frag
: fragment shader..geom
: geometry shader..tesc
: tessellation control shader..tese
: tessellation evaluation shader..comp
: compute shader.
Uruchamianie kompilatora referencyjnego GLSL jest tak proste, jak:
glsllangvalidator shaderFile.vert
Zauważ, że jeśli nie wykryje błędu, nie zwraca on żadnych danych wyjściowych. Uruchomienie kompilatora referencyjnego GLSL na niepoprawnym kodzie Vertex Shadera daje następujące wyniki:
Nie pokaże ci subtelnych różnic pomiędzy kompilatorami GLSL AMD, NVidia lub Intela, ani nie pomoże ci całkowicie usunąć wszystkie błędy z twoich shaderów, ale przynajmniej pomoże ci sprawdzić twoje shadery względem specyfikacji GLSL.
Wyjście Framebuffera
Inną użyteczną sztuczką do zestawu narzędzi do debugowania jest wyświetlanie zawartości bufora ramki w pewnym predefiniowanym regionie twojej aplikacji OpenGL. Prawdopodobnie będziesz często korzystał z framebufferów, a ponieważ większość ich magii dzieje się za kulisami, czasami trudno jest zorientować się, co się dzieje. Wyświetlanie zawartości ramki bufora ramki w aplikacji jest przydatną opcją pozwalającą szybko sprawdzić, czy wszystko wygląda poprawnie.
Zauważ, że wyświetlanie zawartości (załączników) bufora ramki, jak wyjaśniono tutaj, działa tylko na załącznikach tekstur, a nie obiektach bufora renderowania (ang. renderbuffer).
Za pomocą prostego shadera wyświetlającego tylko teksturę możemy łatwo napisać małą funkcję pomocniczą, aby szybko wyświetlić dowolną teksturę w prawym górnym rogu ekranu:
// vertex shader
#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec2 texCoords;
out vec2 TexCoords;
void main()
{
gl_Position = vec4(position, 0.0f, 1.0f);
TexCoords = texCoords;
}
// fragment shader
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D fboAttachment;
void main()
{
FragColor = texture(fboAttachment, TexCoords);
}
void DisplayFramebufferTexture(GLuint textureID)
{
if(!notInitialized)
{
// initialize shader and vao w/ NDC vertex coordinates at top-right of the screen
[...]
}
glActiveTexture(GL_TEXTURE0);
glUseProgram(shaderDisplayFBOOutput);
glBindTexture(GL_TEXTURE_2D, textureID);
glBindVertexArray(vaoDebugTexturedRect);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
glUseProgram(0);
}
int main()
{
[...]
while (!glfwWindowShouldClose(window))
{
[...]
DisplayFramebufferTexture(fboAttachment0);
glfwSwapBuffers(window);
}
}
Daje to ładne małe okienko w rogu ekranu do debugowania wyjścia bufora ramki. Przydatne, na przykład, do określenia, czy wektory normalne przejścia geometrii w odroczonym rendererze wyglądają poprawnie:
Można oczywiście rozszerzyć taką funkcję pomocniczą, aby obsługiwała renderowanie więcej niż jednej tekstury. Jest to szybki i mało elegancki sposób na uzyskanie ciągłej informacji zwrotnej wszystkiego, co znajduje się w buforze ramki.
Zewnętrzne oprogramowanie do debugowania
Kiedy wszystko inne zawiedzie, nadal istnieje możliwość skorzystania z narzędzia innej firmy, aby pomóc nam w naszych działaniach związanych z debugowaniem. Aplikacje innych firm często wstrzykują siebie do sterownika OpenGL i są w stanie przechwytywać wszystkie rodzaje wywołań OpenGL, aby zapewnić szeroki wachlarz interesujących danych dotyczących twojej aplikacji OpenGL. Narzędzia te mogą pomóc na wiele sposobów: profilowanie użycia funkcji OpenGL, znajdowanie wąskich gardeł, sprawdzanie pamięci bufora i wyświetlanie tekstur oraz załączników bufora ramki. Kiedy pracujesz nad (dużym) projektem, tego rodzaju narzędzia mogą stać się nieocenione w procesie tworzenia aplikacji.
Poniżej wymieniono niektóre z bardziej popularnych narzędzi do debugowania; wypróbuj kilka z nich, aby zobaczyć, które najlepiej pasuje do Twoich potrzeb.
RenderDoc
RenderDoc jest świetnym (całkowicie open source’owym) samodzielnym narzędziem do debugowania. Aby rozpocząć przechwytywanie, określ plik wykonywalny (.exe), który chcesz przechwycić, oraz katalog roboczy. Aplikacja działa tak jak zwykle, a gdy chcesz przejrzeć konkretną ramkę, RenderDoc przechwytuje jedną lub więcej klatek w bieżącym stanie pliku wykonywalnego. W przechwyconych klatkach możesz zobaczyć stan potoku, wszystkie polecenia OpenGL, pamięć bufora i używane tekstury.
CodeXL
CodeXL to narzędzie do debugowania GPU wydane zarówno jako samodzielne narzędzie, jak i wtyczka do Visual Studio. CodeXL zapewnia dobry zestaw informacji i doskonale nadaje się do profilowania aplikacji graficznych. CodeXL działa również na kartach NVidia lub Intel, ale bez obsługi debugowania OpenCL.
Nie mam dużego doświadczenia z korzystaniem z CodeXL, ponieważ osobiście uznałem, że RenderDoc jest łatwiejszy w użyciu, ale dodałem go mimo wszystko, ponieważ wygląda na całkiem solidne narzędzie i został głównie opracowany przez jednego z większych producentów GPU.
NVIDIA Nsight
Popularne narzędzie NVIDIA Nsight do debugowania GPU nie jest samodzielnym narzędziem, ale wtyczką do Visual Studio IDE lub Eclipse IDE. Wtyczka Nsight jest niesamowicie przydatnym narzędziem dla programistów grafiki, ponieważ zapewnia wiele statystyk dotyczących czasu pracy procesora oraz stanu GPU klatka po klatce.
W momencie uruchomienia aplikacji z poziomu Visual Studio (lub Eclipse) przy użyciu poleceń debugowania lub profilowania Nsight będzie działać w samej aplikacji. Wspaniałą cechą programu NSight jest to, że renderuje ona nakładki GUI z poziomu aplikacji, które można wykorzystać do gromadzenia wszelkiego rodzaju interesujących informacji o aplikacji, zarówno w czasie wykonywania, jak i podczas analizy klatka po klatce.
Nsight to niezwykle użyteczne narzędzie, które moim zdaniem przewyższa inne narzędzia wymienione powyżej, ale ma jedną poważną wadę - działa tylko na kartach NVIDIA. Jeśli pracujesz na kartach NVIDIA (i korzystasz z Visual Studio), zdecydowanie warto dać Nsight szansę.
Jestem pewien, że jest jeszcze kilka innych narzędzi do debugowania (niektóre, które przychodzą mi na myśl, to narzędzie Valve’a VOGL i APItrace), ale uważam, że ta lista powinna już dawać ci mnóstwo narzędzi do wyboru. Nie jestem ekspertem w żadnym z wyżej wymienionych narzędzi, więc daj mi znać w komentarzach, jeśli podałem gdzieś błędne informacje, a ja w razie potrzeby z radością je poprawię.
Dodatkowe materiały
- Why is your code producing a black window: lista ogólnych przyczyn autorstwa Reto Koradi, dlaczego ekran może nie generować żadnych wyników.
- Debug Output: rozbudowany artykuł o debug output autorstwa Vallentin Source ze szczegółowymi informacjami na temat konfigurowania kontekstu debugowania w wielu systemach okienkowych.