Szybkie wyświetlanie bitmapy na ekranie (GDI / GDI+)

0

Mam spory problem z regularnym wyświetlaniem klatek obrazu (bitmap) na ekranie. Chcąc poprawić wydajność, całkowicie porzuciłem VCL i jako DC (device context) używam okienka stworzonego przy pomocy czystego WinApi. Próbowałem wyświetlać bitmapę na dwa sposoby, przy pomocy starszego GDI i nowszego GDI+.

GDI

{Procedurka odpowiedzialna za tworzenie okna}

//MainDC i BufferDC to HWND
//BMP i ObjectKeeper to HBITMAP
//Handle to uchwyt okna

MainDC := GetDC(Handle);
BufferDC := CreateCompatibleDC(0);
BMP := LoadImage(hInstance, 'D:\Test.bmp', IMAGE_BITMAP, 1920, 1080, LR_LOADFROMFILE);
ObjectKeeper := SelectObject(BufferDC, BMP);

{Obsługa komunikatu WM_PAINT}

BeginPaint(Handle, PS);
BitBlt(MainDC, 0, 0, 1920, 1080, BufferDC, 0, 0, SRCCOPY);
EndPaint(Handle, PS)

GDI+

{Procedurka odpowiedzialna za tworzenie okna}

//Graphics to klasa TGPGraphics
//IMG to klasa TGPImage

MainDC := GetDC(Handle);
Graphics := TGPGraphics.Create(MainDC);
IMG := TGPImage.Create('D:\Test.bmp', False);

{Obsługa komunikatu WM_PAINT}

BeginPaint(Handle, PS);
Graphics.DrawImage(IMG, 0, 0);
EndPaint(Handle, PS)

Wnioski:

  1. GDI jest znacznie szybszy przy rysowaniu dużych powierzchni. Im większy obszar roboczy okna, tym rysowanie trwa krócej. Np. dla okienka 300x300 trwa to u mnie ok.6 ms, ale już w trybie pełnoekranowym, przy rozdzielczości 1080p, ok. 1 ms.
  2. GDI+ działa odwrotnie, im mniejsze okno, tym rysowanie trwa krócej, ale ogólnie jakby wolniej, niż w przypadku zwykłego GDI.
  3. W obu przypadkach czas rysowania jest bardzo niestabilny. Raz okno rysuje się 1 milisekundę, za chwilę już 3, by następnie skoczyć do 5 i potem wrócić do około 1.

Pytania:

  1. Czy ktoś, kto ogarnia GDI/GDI+ mógłby mi doradzić, jak przy tych bibliotekach uzyskać w miarę sensowną i konsekwentną szybkość wyświetlania bitmapek na ekranie, niezależnie od wielkości okna?
  2. Czy znacie może jakiś alternatywny sposób, na osiągnięcie mojego zamierzonego celu? Zaznaczam, że chcę to załatwić software'owo, a więc bez użycia bibliotek, wspieranych przez GPU (a więc np. Direct2D, Direct3D, OpenGL itd.).

Głównie ten rozstrzał czasu rysowania mi przeszkadza. Mam silnik, który w zamyśle ma wyświetlać 60 klatek na sekundę, czyli ok. 16 ms na klatkę. Całość działa tak, że klatka jest renderowana najszybciej jak się da (np. w 9 ms), następnie silnik czeka w bezczynności do pełnych 16 ms (a więc na tym przykładzie, przez 7 ms), by następnie wyświetlić ją na ekranie. Tyle że przecież rysowanie na ekranie też swoje trwa, więc wypadałoby ten czas uwzględnić w obliczeniach. Tylko jak tu uwzględnić coś, co raz trwa 1 ms, a innym razem 5 ms?

Teoretycznie mógłbym to obejść (przynajmniej częściowo) w ten sposób, że silnik zamiast czekać do pełnych 16 ms, mógłby rysować kolejne klatki zaraz po wyrenderowaniu, tyle że sam bufor klatkowy byłby odświeżany co 16 ms. W ten sposób mimo nieregularnej liczby klatek na sekundę, obraz zachowywałby płynność stałych 60 klatek, a niektóre byłby po prostu wyświetlane wielokrotnie. Minus jest jednak taki, że całość dużo bardziej obciąży CPU, więc to też nie jest idealne rozwiązanie.

No ale niezależnie od implementacji powyższego, chciałbym najpierw nieco zoptymalizować to rysowanie. Wszelkie sugestie mile widziane.

0

Z optymalizacją renderowania obrazu niestety nie pomogę, bo nie mam doświadczenia w wykorzystywaniu GDI – nigdy nie używałem, więc nie będę mędrkował. Jednak zwróć uwagę na pewną rzecz – im większa bitmapa, tym dłużej zajmie jej narysowanie. Taka prawidłowość jest logiczna i nie da się tego przeskoczyć, natomiast wahania pomiarów mogą mieć różne podłoże lub mieć związek z implementacją funkcji w bibliotece (nie wiadomo jak ich kod wygląda, więc nie wiadomo co konkretnie robią w każdym z przypadków).

Crow napisał(a):

Głównie ten rozstrzał czasu rysowania mi przeszkadza. Mam silnik, który w zamyśle ma wyświetlać 60 klatek na sekundę, czyli ok. 16 ms na klatkę. Całość działa tak, że klatka jest renderowana najszybciej jak się da (np. w 9 ms), następnie silnik czeka w bezczynności do pełnych 16 ms (a więc na tym przykładzie, przez 7 ms), by następnie wyświetlić ją na ekranie. Tyle że przecież rysowanie na ekranie też swoje trwa, więc wypadałoby ten czas uwzględnić w obliczeniach. Tylko jak tu uwzględnić coś, co raz trwa 1 ms, a innym razem 5 ms?

Wszystko zależy od specyfiki samego silnika. Możesz zacząć od najprostszej wersji, czyli jednowątkowa naprzemienna obsługa klatki i oczekiwania na kolejną klatkę. Czyli aktualizujemy logikę, renderujemy klatkę i czekamy na kolejną klatkę, aby zaktualizować logikę, wyrenderować klatkę i znów czekać na kolejną.

Wiem że przynudzam, ale znów posłużę się moim platformerem jako przykładem. Silnik tej gry utrzymuje stały framerate 60fps przez całą sesję, bez względu na czas renderowania klatek – niektóre z prostą zawartością (np. podczas animacji cutscenek) renderowane są w czasie 3ms, a renderowanie niektórych wymagających złożonego przetwarzania grafik (np. podczas rozgrywki) zjada np. 14ms, co nijak nie wpływa na płynność.

Całość działa tak, że klatka jest renderowana najszybciej jak się da (np. w 9 ms), następnie silnik czeka w bezczynności do pełnych 16 ms (a więc na tym przykładzie, przez 7 ms), by następnie wyświetlić ją na ekranie.

Klatkę możesz najpierw namalować, a później oczekiwać na kolejną, aby uprościć obliczenia. Chyba że klatka ma być renderowana na ekranie w określonym czasie (np. ze względu na obsługę synchronizacji pionowej) to wtedy logika nieco się komplikuje.

Minus jest jednak taki, że całość dużo bardziej obciąży CPU, więc to też nie jest idealne rozwiązanie.

Tego nie za bardzo rozumiem. CPU jest obciążany tylko wtedy, gdy wykonuje cały zestaw instrukcji (np. przetwarzanie grafik), natomiast oczekiwanie na aktualizację końcowego bufora czy ogólnie na następną klatkę nie powinno zużywać jego mocy (chyba że implementujesz precyzyjny zegar bez użycia funkcji Sleep).


W każdym razie kluczem do rozwiązania Twojego problemu ze stabilnością framerate'u jest odpowiedni algorytm, polegający na pomiarze czasu obsługi logiki i generowania klatki (jeden pomiar) oraz aktualizowania zawartości ekranu (drugi pomiar). Na podstawie uzyskanych wartości możesz wykonać dwie przerwy pomiędzy tymi operacjami.

Jeśli wykorzystasz prostszą wersję, czyli wszystko zrobisz od razu (zaktualizujesz logikę, wyrenderujesz klatkę i namalujesz ją na ekranie) to wystarczy zmierzyć ile to wszystko trwało i obliczyć czas oczekiwania na kolejną klatkę.

Aby móc stworzyć precyzyjny mechanizm, polecam skorzystać z funkcji QueryPerformanceFrequency na podstawie wartości przez nią zwróconej obliczyć ile cykli mieści się w jednej klatce, a także z funkcji QueryPerformanceCounter, aby odczytywać wartość licznika dla każdej klatki i na jej podstawie obliczać bardzo dokłady czas przerwy pomiędzy wykonywaniem kodu przez silnik.

Nie zapomnij też o wymuszeniu dokładniejszej pracy shedulera, czyli skorzystaniu z funkcji TimeBeginPeriod i ustawieniu minimalnej wartości pozyskanej za pomocą TimeGetDevCaps. Jeśli potrzebujesz przykładu to mogę Ci pokazać jak tego używać.

0
furious programming napisał(a):

Jednak zwróć uwagę na pewną rzecz – im większa bitmapa, tym dłużej zajmie jej narysowanie.

No ale właśnie w przypadku GDI jest odwrotnie, im większa bitmapa (a właściwie nie bitmapa, tylko DC, na którym ma zostać narysowana), tym rysowanie trwa krócej :). Dziwne, ale tak wychodzi z pomiarów.

furious programming napisał(a):

Wszystko zależy od specyfiki samego silnika. Możesz zacząć od najprostszej wersji, czyli jednowątkowa naprzemienna obsługa klatki i oczekiwania na kolejną klatkę. Czyli aktualizujemy logikę, renderujemy klatkę i czekamy na kolejną klatkę, aby zaktualizować logikę, wyrenderować klatkę i znów czekać na kolejną.

No ja robię tak:

  1. Aktualizowanie logiki.
  2. Renderowanie klatki zgodnie ze stanem danej sceny. Przez renderowanie rozumiem wepchanie ciągu pikseli do bufora (tablicy jednowymiarowej), a następnie przekonwertowanie go do bitmapy gotowej do wyświetlenia na ekranie.
  3. Odczyt czasu renderowania klatki (np. 10 ms).
  4. Oczekiwanie pozostałego czasu w bezczynności (na tym przykładzie 6 ms).
  5. Narysowanie bitmapy na DC. Czyli po prostu wysyłam polecenie: narysuj mi teraz bitmapę, którą trzymasz w pamięci. Tylko skąd mam wiedzieć, ile upłynie czasu między wysłaniem takiego polecenia do systemu, a momentem, kiedy moja bitmapa faktycznie pojawi się na ekranie użytkownika? Jeżeli polecenie wyślę dokładnie 16 ms od czasu wyświetlenia ostatniej klatki, a jej narysowanie na ekranie zajmie np. 3 ms, to użytkownik zobaczy klatkę nie po 16 ms, tylko po 19 ms. Sęk w tym, że nie mogę uwzględnić tego w obliczeniach, bo nie wiem, ile każdorazowo potrwa narysowanie klatki na ekranie. Jeżeli np. 3 ms, to polecenie narysowania powinienem wysłać już po 13 ms od poprzedniej klatki, ale jeżeli rysowanie zajmie tym razem np. tylko 1 ms, to polecenie powinienem wysłać po 15 ms. itd.
furious programming napisał(a):

Klatkę możesz najpierw namalować, a później oczekiwać na kolejną, aby uprościć obliczenia.

Ale przecież nie mogę namalować klatki wcześniej, bo wtedy zamarzę tą, którą użytkownik aktualnie ma na ekranie i tym samym zaburzę płynność wyświetlania.

furious programming napisał(a):

Tego nie za bardzo rozumiem. CPU jest obciążany tylko wtedy, gdy wykonuje cały zestaw instrukcji (np. przetwarzanie grafik), natomiast oczekiwanie na aktualizację końcowego bufora czy ogólnie na następną klatkę nie powinno zużywać jego mocy (chyba że implementujesz precyzyjny zegar bez użycia funkcji Sleep).

Chodzi o to, że obecnie mój silnik przez ten czas oczekiwania nie obciąża procesora (jako że korzystam z wielowątkowości, wątek czeka poprzez WaitFor). Jeżeli zaimplementuję ten pomysł opisany powyżej, wtedy wątek nie będzie na nic nigdy czekać, tylko wypluwać tyle klatek, ile zdoła, a więc np. 150 czy 200 na sekundę, z tym że niektóre, kolejne klatki będą po prostu identyczne, bo buffor będzie się odświeżać co stałe 16 ms. Jeżeli więc silnik zechce wypluć kolejną klatkę przed upływem tego czasu, po prostu narysuje tą samą klatkę co poprzednio. Ewentualnie mógłbym zaimplementować kontrolkę do sprawdzania stanu bufora, na podstawie której silnik mógłby pomijać rysowanie zdublowanych klatek, ale to by chyba wróciło mnie do punktu wyjścia.

furious programming napisał(a):

W każdym razie kluczem do rozwiązania Twojego problemu ze stabilnością framerate'u jest odpowiedni algorytm, polegający na pomiarze czasu obsługi logiki i generowania klatki (jeden pomiar) oraz aktualizowania zawartości ekranu (drugi pomiar). Na podstawie uzyskanych wartości możesz wykonać dwie przerwy pomiędzy tymi operacjami.

No właśnie, tylko co mam poradzić na to, że czas aktualizowania zawartości ekranu (czyli po mojemu: rysowanie na ekranie), za każdym razem się zmienia?

furious programming napisał(a):

Jeśli wykorzystasz prostszą wersję, czyli wszystko zrobisz od razu (zaktualizujesz logikę, wyrenderujesz klatkę i namalujesz ją na ekranie) to wystarczy zmierzyć ile to wszystko trwało i obliczyć czas oczekiwania na kolejną klatkę.

No tego nie rozumiem, bo nie wiem jak miałbym uwzględnić czas rysowania na ekranie (który jest zmienny), w czasie oczekiwania na kolejną klatkę.

furious programming napisał(a):

Aby móc stworzyć precyzyjny mechanizm, polecam skorzystać z funkcji QueryPerformanceFrequency na podstawie wartości przez nią zwróconej obliczyć ile cykli mieści się w jednej klatce, a także z funkcji QueryPerformanceCounter, aby odczytywać wartość licznika dla każdej klatki i na jej podstawie obliczać bardzo dokłady czas przerwy pomiędzy wykonywaniem kodu przez silnik. Nie zapomnij też o wymuszeniu dokładniejszej pracy shedulera, czyli skorzystaniu z funkcji TimeBeginPeriod i ustawieniu minimalnej wartości pozyskanej za pomocą TimeGetDevCaps. Jeśli potrzebujesz przykładu to mogę Ci pokazać jak tego używać.

Korzystam i właśnie przy ich pomocy wykonuję pomiary :).

0
Crow napisał(a):

No ale właśnie w przypadku GDI jest odwrotnie, im większa bitmapa (a właściwie nie bitmapa, tylko DC, na którym ma zostać narysowana), tym rysowanie trwa krócej :). Dziwne, ale tak wychodzi z pomiarów.

Dziwne to, ale nie mam póki co argumentów aby to obalić. ;)

No ja robię tak:

  1. Aktualizowanie logiki.
  2. Renderowanie klatki zgodnie ze stanem danej sceny. Przez renderowanie rozumiem wepchanie ciągu pikseli do bufora (tablicy jednowymiarowej), a następnie przekonwertowanie go do bitmapy gotowej do wyświetlenia na ekranie.
  3. Odczyt czasu renderowania klatki (np. 10 ms).
  4. Oczekiwanie pozostałego czasu w bezczynności (na tym przykładzie 6 ms).

Nie możesz oczekiwać na renderowanie tych 6ms (punkt 4.), bo w ten sposób zużyjesz całe 16ms na generowanie klatki bez jej renderowania – przecież renderowanie klatki na ekranie też trwa, więc wykraczasz poza przydzielony czas.

Poza tym Twój sposób generowania obrazu gry na ekranie powoduje, że gracz zawsze widzi przeszłość – obraz sprzed iluś tam milisekund. Tu nie chodzi o to, że ten lag będzie widoczny (ludzkie oko nie ma takiej czułości aby go wykryć), ale utrudniasz sobie implementację przez dodatkowe ficzery, z których nie masz żadnych realnych benefitów.

  1. Narysowanie bitmapy na DC. Czyli po prostu wysyłam polecenie: narysuj mi teraz bitmapę, którą trzymasz w pamięci. Tylko skąd mam wiedzieć, ile upłynie czasu między wysłaniem takiego polecenia do systemu, a momentem, kiedy moja bitmapa faktycznie pojawi się na ekranie użytkownika?

No dokładnie – nie wiesz ile to potrwa, w dodatku czas rysowania bufora na ekranie ciągle się zmienia, a więc przez to nie masz możliwości obliczenia czasu, w którym silnik ma nic nie robić (czyli oczekiwać). Dlatego powinieneś zaprogramować silnik tak jak Ci zasugerowałem, bo nie dość że upraszcza on logikę silnika, to w dodatku działa tak jak oczekujesz (a żywym przykładem jest mój platformer).

Lista kroków jest prosta:

  • pobierz stan licznika zegara funkcją QPC,
  • zaktualizuj logikę,
  • wygeneruj klatkę w pomocniczym buforze,
  • namaluj bufor na ekranie,
  • znów pobierz stan licznika zegara funkcją QPC,
  • oblicz ile cykli zajęło wygenerowanie klatki i ile czasu należy odczekać,
  • odczekaj zadaną liczbę cykli.

Podsumowując, wiedząc ile cykli zajęło wygenerowanie i wyświetlenie klatki na ekranie, możesz bez problemu obliczyć czas do odczekania. Poza tym na ekranie zawsze będzie zwizualizowany aktualny stan logiki, bo obraz zostanie wysłany na ekran od razu po wygenerowaniu klatki (opóźniony jedynie przez czas samego pchania obrazu na ekran).

Chodzi o to, że obecnie mój silnik przez ten czas oczekiwania nie obciąża procesora (jako że korzystam z wielowątkowości, wątek czeka poprzez WaitFor).

Nie wspominałeś wcześniej o wielowątkowości… To trochę komplikuje logikę silnika. Ale jeśli używasz wątku pobocznego tylko po to, aby oszczędzać moc CPU podczas oczekiwania pomiędzy klatkami, to równie dobrze możesz taki mechanizm zaklepać bez dodatkowego wątku – platformer taki posiada. ;)

Aby nie tracić czasu, napisz do czego wykorzystujesz wątek poboczny – wtedy się zastanowimy nad rozwiązaniem.

0

OK, rozumiem, spróbuję zaimplementować taki mechanizm renderowania, dziękuję :).

Natomiast jeszcze wracając do tego wyświetlania, nie byłoby problemu, gdyby nie straszna chwiejność czasu rysowania. Puściłem samo rysowanie w pętli i o ile np. 10-15 kolejnych wyświetleń odbywało się w akceptowalnym czasie (między 1-2 ms), tak po nich zawsze wyskakiwała jedna, której narysowanie trwało np. 9-10 ms, czyli o wiele, wiele za długo. Musi być jakiś sposób, żeby to ustabilizować...

Jakich bibliotek używasz w swoim platformerze? Lazarus ma jakiś ekwiwalent VCL.Graphics? Może możesz mi polecić jakąś alternatywę dla GDI/GDI+? A może da radę obyć się w ogóle bez takich bibliotek albo napisać własne?
Nie ukrywam, że wzorem technologicznym jest dla mnie Unreal Gold, gra sprzed 20 lat, która dziś, przy renderowaniu software'owym, w 1080p wyciąga u mnie kilkaset klatek na sekundę, a użycie CPU nie przekracza 15%. Widać pewne niedoskonałości (np. migające tekstury czy brak mipmap), ale to i tak jest genialne. Wątpię, żeby oni używali tam windowsowych bibliotek i chciałbym poznać ich sekret ;s.

A co do wielowątkowości, to moja poprzednia wersja silnika korzystała z niej do rasteryzacji trójkątów i pokrywania ich teksturami wewnątrz bufora (całość w 3D). Teraz piszę nową wersję i nie jestem jeszcze pewien, czy nie rozszerzę zakresu użycia wielowątkowości także na inne zastosowania.

EDIT:

Z tym rysowaniem klatek nie daje mi spokoju, więc przygotowałem na tę okoliczność mały schemacik :).

title

Kolor czerwony: czas renderowania klatki.
Kolor jasnozielony: czas potrzebny do wyświetlania klatki na ekranie.
Kolor ciemnozielony: moment, w którym klatka pojawia się na ekranie użytkownika.
Kolor siwy: bezczynne oczekiwanie na nadejście kolejnej klatki (tj. zamknięcie interwału 16 ms i rozpoczęcie procesu od nowa).

Klamerki u dołu obrazują, że przy proponowanym przez ciebie modelu renderowania, obraz na ekranie użytkownika będzie się pojawiać w zmiennych odstępach czasu. Raz klatka będzie "wisieć" na ekranie użytkownika przez np. 6 ms, innym razem przez 12 itd. To chyba nie będzie najlepsze dla płynności?

0

Przy tak krótkich czasach zwykły jednostkowy pomiar jest mało wiarygodny.
Odpal 1000 razy i policz średnią.

1

@_13th_Dragon: no właśnie w tym rzecz – gra raz generuje klatkę szybko, a raz wolno, i właśnie dla tak niestabilnego mechanizmu trzeba zaimplementować stabilny system odmierzania czasu. ;)

0
Crow napisał(a):

Raz klatka będzie "wisieć" na ekranie użytkownika przez np. 6 ms, innym razem przez 12 itd. To chyba nie będzie najlepsze dla płynności?

No nie, aż tak dobrze to nie ma – czas wiszenia obrazu na ekranie jest zależny od karty graficznej i częstotliwości odświeżania obrazu. Nawet jeśli tysiąc razy w jednej sekundzie namalujesz bitmapę na ekranie, to ona fizycznie pojawi się na ekranie np. 60 razy, jeśli monitor pracuje z częstotliwością 60Hz. Tak więc tego typu nieregularność wypychania klatek na ekran nie będzie zauważalna (no, może czasami, gdy spóźnisz się na synchronizację).

Jeśli chcesz wysyłać klatki na ekran w równych odstępach czasu to:

  • pobierz stan licznika zegara funkcją QPC,
  • namaluj bieżącą klatkę na ekranie,
  • wykonaj logikę,
  • wygeneruj nową klatkę,
  • pobierz stan licznika zegara funkcją QPC,
  • oblicz ile cykli zajęło wygenerowanie klatki i ile czasu należy odczekać,
  • odczekaj zadaną liczbę cykli.

Przy tego typu silniku, równomiernie odpalana jest zawsze pierwsza ”instrukcja” w bloku kodu metody silnika. Przy czym nie musisz pamiętać bieżącej klatki, aby móc wygenerować nową – wszystko rób na jednym buforze. IMO powinno działać bez żadnych problemów.

Natomiast jeśli chcesz zrobić konkretny silnik to wzoruj się na tych istniejących, które potrafią obsługiwać zmienny i ograniczony framerate oraz wykorzystują synchronizację pionową, aby uniknąć efektu rozrywania obrazu (i aby animacje zawsze były płynne). Tego typu silniki wykorzystują wielowątkowość, więc są bardziej skomplikowane od silnika mojego autorstwa.


Crow napisał(a):

Jakich bibliotek używasz w swoim platformerze? Lazarus ma jakiś ekwiwalent VCL.Graphics? Może możesz mi polecić jakąś alternatywę dla GDI/GDI+? A może da radę obyć się w ogóle bez takich bibliotek albo napisać własne?

No właśnie w tym rzecz że nie używam żadnych zewnętrznych bibliotek. :)

Używam wyłącznie klas z biblioteki standardowej i LCL, czyli wszystkiego co służy do budowania zwykłych aplikacji okienkowych, a nie gier. W praktyce zapewne coś tam jest z GDI używane, ale musiałbym sprawdzić kod widgetsetu, aby wiedzieć którą funkcję z systemowego API używa np. Canvas.StretchDraw.

W każdym razie cały silnik napisany jest od zera, a także wszystkie funkcje wykorzystywane podczas renderowania klatek (kopiowanie warstw i ich koloryzowanie, obsługa przezroczystości, kopiowanie sprajtów itd.).

Tylko pamiętaj, że ten mój platformer powstał dla zabawy – normalnej gry bym w ten sposób nie pisał.

0

A czy próbowałeś FireMonkey ?

0
PrzemysławWiśniewski napisał(a):

A czy próbowałeś FireMonkey ?

Masz na myśli biblioteki FMX? Myślałem nad tym, ale jeszcze nie testowałem. Warto?

@furious programming

A mógłbyś mi podać dokładny czas (w milisekundach) rysowania klatki na ekranie (pomijając proces jej renderowania, samo wyświetlenie), w 1080p, w twoim platformerze? Z moich testów wynika, że najszybciej działa BitBlt ze zwykłego GDI (ok. 1 ms przy 1080p). Domyślne Canvas.Draw z VCL.Graphics, to jakieś 2-3 ms, przy tej samej rozdzielczości (czyli mimo wszystko sporo wolniej), a na GDI+, mimo użycia specjalnego CachedBitmap i funkcji DrawCachedBitmap (które według MSDN powinny zasuwać jak rakieta), trwa bite 9-10 ms. Ciekaw jestem, jak to wygląda u ciebie :).

0
Crow napisał(a):

A mógłbyś mi podać dokładny czas (w milisekundach) rysowania klatki na ekranie (pomijając proces jej renderowania, samo wyświetlenie), w 1080p, w twoim platformerze?

Jak znajdę chwilkę to sprawdzę, bo aby to zrobić, muszę dopisać kod wykonujący takie pomiary i sprawdzić na innym pececie (ten na którym pracuję ma za małą rozdziałkę), który obsługuje rozdzielczość 1600x900 pikseli. Mój platformer nie wspiera klasycznych rozdzielczości – albo wyświetla okno w rozmiarze klatki, albo dwukrotnie lub trzykrotnie większe, albo rozciągnięte na cały ekran (z zachowaniem proporcji lub bez). Sprawdzę ostatni przypadek, bo jest najbardziej kosztowny.

Przy czym do namalowania rozciągniętej klatki na płótnie formularza używam Canvas.StretchDraw, który pod spodem (po wielu wielu zagłębieniach) wywołuje systemową funkcję StretchMaskBlt. A funkcja ta wołana jest w metodzie Draw klasy TRasterImage. Natomiast renderowanie obrazu klatki wykonywane jest wyłącznie przy użyciu własnych metod operujących na bitmapach, więc tutaj nie ma co testować.

Pamiętaj jednak, że wyniki pomiarów przedstawione w milisekundach nic sensownego nie przedstawią, dlatego że na różnych pecetach wyniki będą różne z racji zupełnie różnych procesorów. No ale sprawdzić mogę – z ciekawości. ;)

0
Crow napisał(a):

Zaznaczam, że chcę to załatwić software'owo, a więc bez użycia bibliotek, wspieranych przez GPU (a więc np. Direct2D, Direct3D, OpenGL itd.).

Zależy ci na wydajności, ale narzucasz sobie takie dziwne i niezrozumiałe ograniczenie.

Renderować bitmapę możesz sobie dowolnym sposobem, ale nie ma powodu aby odrzucać np. Direct3D jako API wyświetlające bitmapę na ekranie, jeżeli miałoby to być akurat działać szybciej.

0

@Azarien: dawniej DirectX nie było, a gry (w tym 3D) pisano – fajnie zrobić to samo. :)

0
furious programming napisał(a):

@Azarien: dawniej DirectX nie było, a gry (w tym 3D) pisano – fajnie zrobić to samo. :)

Ale nic nie stoi na przeszkodzie żeby używać Direct3D czy OpenGL jako wyjściowego API jeżeli okaże się że to działa lepiej/szybciej/wygodniej niż na przykład GDI.
Cały czas mówimy o „regularnym wyświetlaniu klatek obrazu”, nie o budowaniu sceny 3D przy użyciu tych bibliotek.

Sam bym pewnie użył do tego DirectDraw, która to nazwa jeszcze w tym wątku nie padła. Tak wiem że jest „deprecated”, ale co z tego, ponadto DDraw znam a GDI nie bardzo.

0

@Azarien: Tak jak furious napisał - bo mogę :). Gdybym chciał zrobić profesjonalną grę, to jasne, wtedy najlepszym wyjściem byłoby nie tylko korzystanie z zaawansowanych bibliotek 3D jak Direct3D czy OpenGL, ale nawet użycie któregoś z darmowych silników (np. Unity), jednak nie taki mam cel. Ja chcę po prostu stworzyć retro-silnik, napędzany metodami sprzed upowszechnienia się akceleracji sprzętowej (przy okazji ucząc się od początku do końca jak wygląda pipeline przy tworzeniu grafiki 3D), który pozwoli mi uzyskać efekt zbliżony do renderowanych software'owo Unreal'a, Quake'a czy Half-Life'a (a niektóre z tych renderów potrafią być zadziwiająco szybkie!).

DirectDraw to chyba raczej niezbyt dobry pomysł. Gdyby sięgać do tej rodziny bibliotek, to już lepiej do Direct2D. Nawet próbowałem, ale ten wrapper od Embarcadero jest jakiś dziwny (stworzyli TDirect2DCanvas obudowując TCustomCanvas i mam wrażenie, że po drodze zgubili z połowę funkcjonalności...), a dokumentacja do niego jest uboga i dziurawa (nawet ich własne przykłady się nie kompilują ;s). Próbowałem to obejść, sięgając bezpośrednio do stosownych struktur, klas i funkcji (korzystając z dokumentacji MSDN), ale całość mi się cały czas wysypywała, mimo że robiłem wszystko zgodnie z zaleceniami (tak jak w przypadku GDI/GDI+, z którymi nie miałem takich problemów).

Jeszcze chyba sprawdzę te FMXy. Ogólnie to ja wielu funkcjonalności nie potrzebuję. Ot, muszę mieć bufor pikseli, który będę mógł łatwo i szybko modyfikować, a potem wyświetlać na ekranie. Całą resztę robię sam, własnymi funkcjami, więc nie potrzebuję żadnej wbudowanej akceleracji rysowania linii czy prymitywów,

2
Crow napisał(a):

@Azarien: Tak jak furious napisał - bo mogę :). Gdybym chciał zrobić profesjonalną grę, to jasne, wtedy najlepszym wyjściem byłoby nie tylko korzystanie z zaawansowanych bibliotek 3D jak Direct3D czy OpenGL, ale nawet użycie któregoś z darmowych silników (np. Unity), jednak nie taki mam cel. Ja chcę po prostu stworzyć retro-silnik, napędzany metodami sprzed upowszechnienia się akceleracji sprzętowej (przy okazji ucząc się od początku do końca jak wygląda pipeline przy tworzeniu grafiki 3D), który pozwoli mi uzyskać efekt zbliżony do renderowanych software'owo Unreal'a, Quake'a czy Half-Life'a (a niektóre z tych renderów potrafią być zadziwiająco szybkie!).

Nadal nie rozumiesz. Mam na myśli użycie D3D tylko jako output, rysowałbyś tylko jeden wielki bitmapowy prostokąt. To co na nim będzie generujesz sobie software'owo jak chcesz.
Jest szansa, choć nie ma gwarancji, że będzie to działać sprawniej niż przez GDI.

Przykładem takiej aplikacji jest DOSBox - to co masz na ekranie jest efektem emulacji, siłą rzeczy wszystko jest renderowane programowo, ale może być (w zależności od ustawień) wyrzucane na ekran przez GDI, DDraw, OpenGL, i co tam jeszcze.

Innym przykładem jest MPC-HC (video player) - wśród różnych trybów renderowania filmów jest VMR-9, używający Direct3D 9 (pozostałe nie wiem czego używają, ale czegoś na pewno)

PS. GDI+ jest znany z tego że jest wolny, raczej nie nadaje się do tego celu.

0

Pozwolę sobie odkopać :).

@Azarien - Napisałeś, że znasz DirectDraw. Mógłbyś podrzucić kilka porad, jak się tym posługiwać? Na MSDN jest szczątkowa dokumentacja, ale Embarcadero nie wspomina nic, jak tego używać pod Delphi.

1

@Crow: możesz pobrać DelphiX i zobaczyć jak działa, a później przeanalizować sobie kod źródłowy w celu poznania sposobu działania. Jeśli nie chcesz używać komponentów i ogólnie wszystkiego wysokopoziomowego co ta biblioteka oferuje, to możesz wyciągnąć co nieco ze źródeł i wykorzystać w swoim projekcie.

1
Crow napisał(a):

Pozwolę sobie odkopać :).

@Azarien - Napisałeś, że znasz DirectDraw. Mógłbyś podrzucić kilka porad, jak się tym posługiwać? Na MSDN jest szczątkowa dokumentacja,

W zasadzie wszystko co wiem pochodzi właśnie stąd:
https://docs.microsoft.com/pl-pl/windows/win32/directdraw/directdraw-reference

plus kilka starych tutoriali znalezionych w necie..

ale Embarcadero nie wspomina nic, jak tego używać pod Delphi.

Z tego co widzę w Delphi 10.1 Berlin nie ma unitu do obsługi DirectDraw, i chyba go nigdy nie było.
Trzeba by znaleźć gotowy unit (z pewnością takie istnieją) albo napisać go samemu, na podstawie dokumentacji i wiedzy o tym jak w Delphi korzystać z interfejsów COM.
To nawet nie takie trudne...

EDIT: zdaje się że unity do obsługi DirectX są w pakiecie JEDI. Poszukaj na https://www.delphi-jedi.org/

1 użytkowników online, w tym zalogowanych: 0, gości: 1