Saya sedang menulis game di C ++ menggunakan OpenGL.
Bagi mereka yang tidak tahu, dengan OpenGL API Anda melakukan banyak panggilan ke hal-hal seperti glGenBuffers
dan glCreateShader
lain-lain. Jenis pengembalian ini GLuint
adalah pengidentifikasi unik untuk apa yang baru saja Anda buat. Benda yang diciptakan hidup dengan memori GPU.
Mengingat Memori GPU terkadang terbatas, Anda tidak ingin membuat dua hal yang sama ketika digunakan oleh banyak objek.
Misalnya, Shader. Anda menautkan Program Shader dan kemudian Anda memiliki GLuint
. Setelah selesai dengan Shader, Anda harus menelepon glDeleteShader
(atau sesuatu yang mempengaruhi).
Sekarang, katakanlah saya memiliki hierarki kelas yang dangkal seperti:
class WorldEntity
{
public:
/* ... */
protected:
ShaderProgram* shader;
/* ... */
};
class CarEntity : public WorldEntity
{
/* ... */
};
class PersonEntity: public WorldEntity
{
/* ... */
};
Kode apa pun yang pernah saya lihat akan mengharuskan semua Konstruktor memiliki ShaderProgram*
izin untuk disimpan dalam WorldEntity
. ShaderProgram
adalah kelas saya yang merangkum pengikatan GLuint
ke keadaan shader saat ini dalam konteks OpenGL serta beberapa hal bermanfaat lainnya yang perlu Anda lakukan dengan Shader.
Masalah yang saya miliki dengan ini adalah:
- Ada banyak parameter yang diperlukan untuk membangun a
WorldEntity
(pertimbangkan bahwa mungkin ada mesh, shader, sekelompok tekstur dll, yang semuanya dapat dibagikan, sehingga dilewatkan sebagai petunjuk) - Apa pun yang menciptakan
WorldEntity
kebutuhan untuk mengetahui apaShaderProgram
yang dibutuhkan - Ini mungkin membutuhkan semacam kelas tegukan
EntityManager
yang tahu contoh apa yangShaderProgram
harus dilewatkan ke entitas yang berbeda.
Jadi sekarang karena ada Manager
kelas-kelas yang perlu mendaftar sendiri EntityManager
bersama dengan ShaderProgram
instance yang mereka butuhkan, atau saya perlu keledai besar switch
di manajer yang perlu saya perbarui untuk setiap WorldEntity
jenis turunan baru .
Pikiran pertama saya adalah membuat ShaderManager
kelas (saya tahu, Manajer buruk) yang saya lewati dengan referensi atau penunjuk ke WorldEntity
kelas sehingga mereka dapat membuat apa pun yang ShaderProgram
mereka inginkan, melalui ShaderManager
dan ShaderManager
dapat melacak ShaderProgram
s yang sudah ada , sehingga dapat kembalikan yang sudah ada atau buat yang baru jika perlu.
(Saya bisa menyimpan ShaderProgram
s melalui hash dari nama file ShaderProgram
kode sumber aktual s)
Jadi sekarang:
- Saya sekarang meneruskan pointer ke
ShaderManager
bukanShaderProgram
, jadi masih ada banyak parameter - Saya tidak memerlukan
EntityManager
, entitas itu sendiri akan tahu apa yangShaderProgram
harus dibuat, danShaderManager
akan menangani yang sebenarnyaShaderProgram
. - Tapi sekarang saya tidak tahu kapan
ShaderManager
bisa dengan aman menghapusShaderProgram
yang dimilikinya.
Jadi sekarang saya telah menambahkan penghitungan referensi ke ShaderProgram
kelas saya yang menghapus internal GLuint
melalui glDeleteProgram
dan saya tidak melakukannya ShaderManager
.
Jadi sekarang:
- Objek dapat membuat apa pun
ShaderProgram
yang dibutuhkannya - Tapi sekarang ada duplikat
ShaderProgram
karena tidak ada Manajer eksternal yang melacak
Akhirnya saya datang untuk membuat satu dari dua keputusan:
1. Kelas Statis
A static class
yang dipanggil untuk membuat ShaderProgram
s. Itu membuat trek internal ShaderProgram
berdasarkan hash dari nama file - ini berarti saya tidak perlu lagi melewati pointer atau referensi ke ShaderProgram
atau ShaderManager
sekitar, jadi lebih sedikit parameter - WorldEntities
Memiliki semua pengetahuan tentang contoh yang ShaderProgram
ingin mereka buat
static ShaderManager
Kebutuhan baru ini untuk:
- pertahankan hitungan berapa kali a
ShaderProgram
digunakan dan sayaShaderProgram
tidak membuat salinan ATAU ShaderProgram
menghitung referensi mereka dan hanya memanggilglDeleteProgram
destruktor mereka ketika hitungan itu0
DANShaderManager
secara berkala memeriksa untukShaderProgram
dengan hitungan 1 dan membuangnya.
Kelemahan dari pendekatan ini yang saya lihat adalah:
Saya memiliki kelas statis global yang mungkin menjadi masalah. Konteks OpenGL perlu dibuat sebelum memanggil
glX
fungsi apa pun . Jadi berpotensi,WorldEntity
mungkin dibuat dan mencoba untuk membuatShaderProgram
sebelum pembuatan Konteks OpenGL, yang akan mengakibatkan crash.Satu-satunya cara untuk mengatasi hal ini adalah kembali menyerahkan semua yang ada sebagai pointer / referensi, atau memiliki kelas global GLContext yang dapat ditanyakan, atau memegang segala sesuatu di kelas yang menciptakan Konteks pada konstruksi. Atau mungkin hanya boolean global
IsContextCreated
yang dapat diperiksa. Tapi saya khawatir ini memberi saya kode jelek di mana-mana.Apa yang bisa saya lihat adalah pindah ke:
- Kelas besar
Engine
yang memiliki setiap kelas lain tersembunyi di dalamnya sehingga dapat mengontrol urutan konstruksi / dekonstruksi dengan tepat. Ini tampak seperti kekacauan besar kode antarmuka antara pengguna mesin dan mesin, seperti pembungkus di atas pembungkus - Seluruh kelas "Manajer" yang melacak instance dan menghapus hal-hal saat diperlukan. Ini mungkin kejahatan yang perlu?
- Kelas besar
DAN
- Kapan sebenarnya membersihkan
ShaderProgram
sstatic ShaderManager
? Setiap beberapa menit? Setiap Loop Game? Saya dengan anggun menangani penyusunan ulang shader dalam kasus ketika sebuahShaderProgram
dihapus tetapi kemudian baruWorldEntity
memintanya; tapi saya yakin ada cara yang lebih baik.
2. Metode yang lebih baik
Itulah yang saya minta di sini
sumber
WorldEntity
s; Bukankah itu menggeser beberapa masalah? Karena sekarang kelas WorldFactory harus lulus setiap WolrdEntity ShaderProgram yang benar.Jawaban:
Permintaan maaf untuk necromancy tapi saya telah melihat begitu banyak tersandung masalah serupa dengan mengelola sumber daya OpenGL, termasuk saya di masa lalu. Dan begitu banyak kesulitan yang saya perjuangkan dengan yang saya kenali pada orang lain datang dari godaan untuk membungkus dan kadang-kadang abstrak dan bahkan merangkum sumber daya OGL yang diperlukan untuk beberapa entitas game analog yang akan diberikan.
Dan "cara yang lebih baik" yang saya temukan (setidaknya satu yang mengakhiri perjuangan khusus saya di sana) adalah melakukan hal-hal yang sebaliknya. Artinya, jangan khawatirkan diri Anda dengan aspek tingkat rendah OGL dalam mendesain entitas dan komponen gim Anda dan menjauhlah dari ide-ide seperti itu yang harus Anda
Model
simpan seperti segitiga dan simpul primitif dalam bentuk objek yang dibungkus atau bahkan abstraksi VBO.Masalah Rendering vs. Masalah Desain Game
Ada beberapa konsep level yang sedikit lebih tinggi daripada tekstur GPU, misalnya, dengan persyaratan manajemen yang lebih sederhana seperti gambar CPU (dan Anda tetap membutuhkannya, setidaknya untuk sementara, sebelum Anda bahkan dapat membuat dan mengikat tekstur GPU). Tidak ada masalah rendering model mungkin cukup hanya menyimpan properti yang menunjukkan nama file yang digunakan untuk file yang berisi data untuk model. Anda dapat memiliki komponen "material" yang tingkatnya lebih tinggi dan lebih abstrak dan menjelaskan sifat-sifat material itu daripada shader GLSL.
Dan kemudian hanya ada satu tempat dalam basis kode yang berkaitan dengan hal-hal seperti shader dan tekstur GPU dan VAOs / VBOs dan konteks OpenGL, dan itulah implementasi dari sistem rendering . Sistem rendering mungkin loop melalui entitas dalam adegan game (dalam kasus saya ini berjalan melalui indeks spasial, tetapi Anda dapat memahami lebih mudah dan memulai dengan loop sederhana sebelum menerapkan optimasi seperti frustum culling dengan indeks spasial), dan itu menemukan komponen tingkat tinggi Anda seperti "bahan" dan "gambar" dan nama file model.
Dan tugasnya adalah untuk mengambil data tingkat yang lebih tinggi yang tidak secara langsung berkaitan dengan GPU dan memuat / membuat / mengasosiasikan / mengikat / menggunakan / memisahkan / menghancurkan sumber daya OpenGL yang diperlukan berdasarkan apa yang ditemukan di tempat kejadian dan apa yang terjadi pada tempat kejadian. Dan itu menghilangkan godaan untuk menggunakan hal-hal seperti lajang dan versi statis "manajer" dan apa yang tidak, karena sekarang semua manajemen sumber daya OGL Anda terpusat ke satu sistem / objek dalam basis kode Anda (walaupun tentu saja Anda dapat menguraikannya menjadi objek lebih lanjut yang dienkapsulasi oleh renderer untuk membuat kode lebih mudah dikelola). Ini juga secara alami menghindari beberapa poin tersandung dengan hal-hal seperti mencoba menghancurkan sumber daya di luar konteks OGL yang valid,
Menghindari Perubahan Desain
Selanjutnya yang menawarkan banyak ruang bernapas untuk menghindari perubahan desain pusat yang mahal, karena katakan Anda menemukan di belakang bahwa beberapa bahan memerlukan beberapa rendering pass (dan beberapa shader) untuk diurai, seperti pass hamburan dan shader untuk permukaan bawah permukaan untuk bahan kulit, padahal sebelumnya Anda ingin mengurutkan bahan dengan shader GPU tunggal. Dalam hal ini tidak ada perubahan desain yang mahal untuk antarmuka pusat yang digunakan oleh banyak hal. Yang Anda lakukan hanyalah memperbarui implementasi sistem rendering secara lokal untuk menangani kasus yang sebelumnya tidak terduga ini ketika berhadapan dengan sifat-sifat kulit pada komponen material tingkat tinggi Anda.
Strategi Keseluruhan
Dan itulah strategi keseluruhan yang saya gunakan sekarang, dan itu terutama menjadi semakin membantu semakin kompleks masalah rendering Anda. Sebagai sisi negatifnya memang membutuhkan sedikit pekerjaan di muka daripada seperti menyuntikkan entitas game Anda dengan shader dan VBO dan hal-hal seperti itu, dan itu juga pasangan renderer Anda lebih ke mesin game khusus Anda (atau abstraksinya, meskipun dalam pertukaran tingkat yang lebih tinggi entitas dan konsep game menjadi sepenuhnya terpisah dari masalah rendering tingkat rendah). Dan penyaji Anda mungkin perlu hal-hal seperti callback untuk memberi tahu ketika entitas dihancurkan sehingga dapat membatalkan dan menghancurkan data yang terkait dengannya (Anda dapat menggunakan penghitungan ulang di sini atau
shared_ptr
untuk sumber daya bersama, tetapi hanya secara lokal di dalam renderer). Dan Anda mungkin ingin beberapa cara yang efisien untuk mengaitkan dan melepaskan semua jenis data yang dirender ke entitas apa pun dalam waktu konstan (ECS cenderung memberikan ini kelelawar untuk setiap sistem dengan cara Anda dapat mengaitkan jenis komponen baru dengan cepat jika Anda memiliki ECS - jika tidak seharusnya tidak terlalu sulit) ... tetapi pada sisi baiknya semua hal-hal ini kemungkinan akan berguna untuk sistem selain renderer pula.Memang implementasi nyata mendapatkan lebih banyak nuansa dari ini dan mungkin mengaburkan hal-hal ini sedikit lebih, seperti mesin Anda mungkin ingin berurusan dengan hal-hal seperti segitiga dan simpul di bidang selain rendering (mis: fisika mungkin ingin data seperti itu untuk melakukan deteksi tabrakan ). Tetapi di mana kehidupan mulai menjadi jauh lebih mudah (setidaknya bagi saya) adalah merangkul pola pikir dan strategi pembalikan seperti ini sebagai titik awal.
Dan mendesain penyaji waktu nyata sangat sulit dalam pengalaman saya - hal tersulit yang pernah saya rancang (dan terus merancang ulang) dengan perubahan cepat pada perangkat keras, kemampuan peneduhan, teknik yang ditemukan. Tetapi pendekatan ini tidak menghilangkan kekhawatiran segera ketika sumber daya GPU dapat dibuat / dihancurkan dengan memusatkan semua itu ke implementasi rendering, dan bahkan lebih menguntungkan bagi saya adalah bahwa itu menggeser apa yang seharusnya menjadi mahal dan perubahan desain cascading (yang dapat tumpah ke dalam kode tidak langsung berkaitan dengan rendering) hanya implementasi renderer itu sendiri. Dan pengurangan biaya perubahan dapat menambah penghematan besar dengan sesuatu yang bergeser dalam persyaratan setiap atau dua tahun secepat rendering waktu nyata.
Contoh Bayangan Anda
Cara saya menangani contoh naungan Anda adalah bahwa saya tidak mementingkan diri sendiri dengan hal-hal seperti shader GLSL dalam hal-hal seperti entitas mobil dan orang. Saya menyibukkan diri dengan "bahan" yang merupakan objek CPU yang sangat ringan yang hanya berisi properti yang menggambarkan jenis bahan apa itu (kulit, cat mobil, dll). Dalam kasus saya yang sebenarnya, ini sedikit canggih karena saya memiliki DSEL yang mirip dengan Unreal Blueprints untuk pemrograman shader menggunakan bahasa visual, tetapi bahan tidak menyimpan pegangan shader GLSL.
Saya dulu melakukan hal serupa ketika saya menyimpan dan mengelola sumber daya semacam ini "di luar angkasa" di luar penyaji karena upaya naif saya yang paling awal yang hanya mencoba untuk langsung menghancurkan sumber daya tersebut dalam sebuah destructor sering mencoba untuk menghancurkan sumber daya di luar konteks GL yang valid (dan kadang-kadang saya bahkan secara tidak sengaja mencoba membuatnya dalam skrip atau sesuatu ketika saya tidak berada dalam konteks yang valid), jadi saya perlu menunda pembuatan dan penghancuran pada kasus-kasus di mana saya dapat menjamin saya berada dalam konteks yang valid yang mengarah ke desain "manajer" serupa yang Anda gambarkan.
Semua masalah ini hilang jika Anda menyimpan sumber daya CPU di tempatnya dan meminta penyaji menangani masalah pengelolaan sumber daya GPU. Saya tidak bisa menghancurkan shader OGL di mana pun, tetapi saya dapat menghancurkan materi CPU di mana saja dan dengan mudah digunakan
shared_ptr
dan sebagainya tanpa membuat saya kesulitan.Sekarang kekhawatiran itu sebenarnya sulit, bahkan dalam kasus saya jika Anda ingin mengelola sumber daya GPU secara efisien dan mengeluarkannya ketika tidak lagi diperlukan. Dalam kasus saya, saya bisa berurusan dengan adegan besar dan saya bekerja di VFX daripada permainan di mana artis mungkin memiliki konten yang intens tidak dioptimalkan untuk rendering realtime (tekstur epik, model yang mencakup jutaan poligon, dll).
Ini sangat berguna untuk kinerja bukan hanya untuk menghindari rendering ketika mereka offscreen (keluar dari tampilan frustrasi) tetapi juga membongkar sumber daya GPU ketika tidak lagi diperlukan untuk sementara waktu (katakanlah pengguna tidak melihat sesuatu jalan keluar di kejauhan ruang untuk sementara waktu).
Jadi solusi yang paling sering saya gunakan adalah solusi "timestamped", meskipun saya tidak yakin seberapa bisa diterapkannya dengan game. Ketika saya mulai menggunakan / mengikat sumber daya untuk rendering (mis: mereka lulus tes culling frustum), saya menyimpan waktu saat ini dengan mereka. Kemudian secara berkala ada pemeriksaan untuk melihat apakah sumber daya tersebut tidak pernah digunakan untuk sementara waktu, dan jika demikian, mereka dibongkar / dihancurkan (meskipun data CPU asli yang digunakan untuk menghasilkan sumber daya GPU disimpan sampai entitas sebenarnya yang menyimpan komponen-komponen tersebut dihancurkan) atau sampai komponen-komponen tersebut dikeluarkan dari entitas). Ketika jumlah sumber daya meningkat dan lebih banyak memori digunakan, sistem menjadi lebih agresif dalam membongkar / menghancurkan sumber daya tersebut (jumlah waktu idle yang diperbolehkan untuk yang lama,
Saya membayangkan itu sangat tergantung pada desain game Anda. Karena jika Anda memiliki game dengan pendekatan yang lebih tersegmentasi dengan level / zona yang lebih kecil, maka Anda mungkin dapat (dan menemukan waktu termudah menjaga frame rate stabil) memuat semua sumber daya yang diperlukan untuk level tersebut di muka dan membongkar mereka ketika pengguna pergi ke level berikutnya. Sedangkan jika Anda memiliki beberapa permainan dunia terbuka besar yang mulus seperti itu, Anda mungkin perlu strategi yang jauh lebih canggih untuk mengontrol kapan membuat dan menghancurkan sumber daya ini, dan mungkin ada tantangan yang lebih besar di sana untuk melakukan ini semua tanpa gagap. Dalam domain VFX saya sedikit cegukan untuk membingkai tingkat tidak sebesar masalah besar (saya mencoba untuk menghilangkan mereka dalam alasan) karena pengguna tidak akan bermain-main sebagai akibatnya.
Semua kerumitan ini dalam kasus saya masih terisolasi ke sistem rendering, dan sementara saya telah menggeneralisasi kelas dan kode untuk membantu mengimplementasikannya, tidak ada kekhawatiran tentang konteks GL yang valid dan godaan untuk menggunakan global atau semacamnya.
sumber
Daripada melakukan penghitungan referensi di
ShaderProgram
kelas itu sendiri, lebih baik untuk mendelegasikan itu ke kelas pointer cerdas, sepertistd::shared_ptr<>
. Dengan begitu, Anda memastikan bahwa setiap kelas hanya memiliki satu pekerjaan yang harus dilakukan.Untuk menghindari melelahkan sumber daya OpenGL Anda secara tidak sengaja, Anda dapat membuat
ShaderProgram
non-copyable (swasta / dihapus copy-constructor dan operator penugasan copy).Untuk menyimpan repositori terpusat dari
ShaderProgram
instance yang dapat dibagi, Anda bisa menggunakanSharedShaderProgramFactory
(mirip dengan manajer statis Anda, tetapi dengan nama yang lebih baik) seperti ini:Kelas pabrik dapat diimplementasikan sebagai kelas statis, Singleton atau ketergantungan yang dilewati jika diperlukan.
sumber
Opengl telah dirancang sebagai pustaka C, dan memiliki karakteristik perangkat lunak prosedural. Salah satu aturan pembuka yang berasal dari menjadi pustaka C terlihat seperti ini:
Ini adalah fitur api terbuka. Pada dasarnya ini mengasumsikan bahwa seluruh kode Anda berada di dalam fungsi main (), dan semua pegangan itu dilewatkan melalui variabel lokal main ().
Kepastian aturan ini adalah sebagai berikut:
sumber
main
tampaknya agak rumit bagi saya (setidaknya dalam kata-kata).