OpenSceneGraph: दृश्य ग्राफ़ और स्मार्ट पॉइंटर्स

छवि

परिचय


पिछले लेख में, हमने ओपनसेकेग्राफ असेंबली को स्रोत से देखा और एक प्राथमिक उदाहरण लिखा जिसमें एक ग्रे विमान एक खाली बैंगनी दुनिया में लटका हुआ है। मैं सहमत हूं, बहुत प्रभावशाली नहीं है। हालांकि, जैसा कि मैंने पहले कहा था, इस छोटे से उदाहरण में, मुख्य अवधारणाएं हैं जिन पर यह ग्राफिक्स इंजन आधारित है। आइए उन पर अधिक विस्तार से विचार करें। नीचे दी गई सामग्री ओएसजी के बारे में अलेक्जेंडर बोबोव के ब्लॉग से दृष्टांतों का उपयोग करती है (यह अफ़सोस की बात है कि लेखक ने ओएसजी के बारे में लिखना छोड़ दिया है)। लेख भी OpenSceneGraph 3.0 पुस्तक से सामग्री और उदाहरणों पर आधारित है शुरुआती गाइड

मुझे कहना होगा कि पिछला प्रकाशन कुछ आलोचना के अधीन था, जिसके साथ मैं आंशिक रूप से सहमत हूं - सामग्री अनसोल्ड रही और संदर्भ से बाहर ले गई। मैं कटौती के तहत इस चूक को ठीक करने की कोशिश करूंगा।

1. दृश्य के ग्राफ और इसके नोड्स के बारे में संक्षेप में


इंजन की केंद्रीय अवधारणा तथाकथित दृश्य ग्राफ है (यह कोई संयोग नहीं है कि यह फ्रेमवर्क के नाम पर ही अटक गया है) - एक पदानुक्रमित पेड़ संरचना जो आपको तीन आयामी दृश्य के तार्किक और स्थानिक प्रतिनिधित्व को व्यवस्थित करने की अनुमति देती है। दृश्य ग्राफ में रूट नोड और उससे जुड़े मध्यवर्ती और टर्मिनल नोड्स या नोड्स होते हैं

उदाहरण के लिए



इस ग्राफ में एक घर और उसमें एक टेबल से मिलकर एक दृश्य दिखाया गया है। घर का एक निश्चित ज्यामितीय प्रतिनिधित्व होता है और मूल नोड (रूट) से जुड़े एक निश्चित बुनियादी समन्वय प्रणाली के सापेक्ष अंतरिक्ष में एक निश्चित तरीके से स्थित होता है। तालिका को कुछ ज्यामिति द्वारा भी वर्णित किया गया है, जो किसी तरह से घर के सापेक्ष स्थित है, और घर के साथ-साथ रूट नोड के सापेक्ष है। सभी नोड्स, एक सामान्य संपत्ति है, क्योंकि वे एक ऑग से विरासत में लेते हैं :: नोड वर्ग, उनके कार्यात्मक उद्देश्य के अनुसार प्रकारों में विभाजित हैं

  1. समूह नोड्स (osg :: Group) - सभी मध्यवर्ती नोड्स के लिए आधार वर्ग हैं और समूहों में अन्य नोड्स को संयोजित करने के लिए डिज़ाइन किए गए हैं
  2. परिवर्तन नोड्स (osg :: ट्रांसफॉर्म और उसके वंशज) - वस्तु निर्देशांक के परिवर्तन का वर्णन करने के लिए डिज़ाइन किया गया
  3. ज्यामितीय नोड्स (osg :: Geode) - एक या अधिक ज्यामितीय वस्तुओं के बारे में जानकारी वाले दृश्य ग्राफ के टर्मिनल (पत्ती) नोड्स।

ओएसजी में दृश्य वस्तुओं की ज्यामिति वस्तु के अपने स्थानीय समन्वय प्रणाली में वर्णित है। इस ऑब्जेक्ट और रूट नोड के बीच स्थित ट्रांसफ़ॉर्मिंग नोड बेस कोऑर्डिनेट सिस्टम में ऑब्जेक्ट की स्थिति प्राप्त करने के लिए मैट्रिक्स कोऑर्डिनेट ट्रांसफॉर्मेशन को लागू करते हैं।

नोड्स कई महत्वपूर्ण कार्य करते हैं, विशेष रूप से, वस्तुओं के प्रदर्शन की स्थिति को संग्रहीत करते हैं, और यह राज्य केवल इस नोड से जुड़े सबग्राफ को प्रभावित करता है। कई कॉलबैक दृश्य ग्राफ, ईवेंट हैंडलर में नोड्स के साथ जुड़े हो सकते हैं जो आपको नोड की स्थिति और संबंधित उप-मार्ग को बदलने की अनुमति देते हैं।

स्क्रीन पर अंतिम परिणाम प्राप्त करने से जुड़े दृश्य ग्राफ पर सभी वैश्विक संचालन, इंजन द्वारा स्वचालित रूप से किए जाते हैं, समय-समय पर ग्राफ को गहराई से ट्रैवर्स करके।

पिछली बार जांच किए गए उदाहरण में, हमारे दृश्य में एक एकल वस्तु शामिल थी - एक फ़ाइल से भरा एक हवाई जहाज का मॉडल। बहुत आगे देखते हुए, मैं कहूंगा कि यह मॉडल दृश्य ग्राफ का पत्ता नोड है। यह इंजन के वैश्विक आधार समन्वय प्रणाली के लिए कसकर वेल्डेड है।

2. OSG मेमोरी प्रबंधन


चूंकि दृश्य ग्राफ के नोड्स दृश्य वस्तुओं और उन पर संचालन के बारे में बहुत सारे डेटा संग्रहीत करते हैं, इसलिए इस डेटा को संग्रहीत करने के लिए, गतिशील रूप से, मेमोरी को आवंटित करना आवश्यक है। इस मामले में, जब दृश्य ग्राफ में हेरफेर किया जाता है, और, उदाहरण के लिए, इसके कुछ नोड्स को हटाते हुए, आपको सावधानीपूर्वक निगरानी करने की आवश्यकता है कि ग्राफ़ के हटाए गए नोड अब संसाधित नहीं हैं। यह प्रक्रिया हमेशा त्रुटियों, समय लेने वाली डिबगिंग के साथ होती है, क्योंकि डेवलपर के लिए यह ट्रैक करना काफी मुश्किल होता है कि कौन सी वस्तुएं मौजूदा डेटा को संदर्भित करती हैं और जिसे हटा दिया जाना चाहिए। प्रभावी स्मृति प्रबंधन के बिना, विभाजन त्रुटियां और मेमोरी लीक होने की अधिक संभावना है।

मेमोरी प्रबंधन OSG में एक महत्वपूर्ण कार्य है और इसकी अवधारणा दो बिंदुओं पर आधारित है:

  1. स्मृति का आवंटन: किसी वस्तु के भंडारण के लिए आवश्यक स्मृति की मात्रा का आवंटन सुनिश्चित करना।
  2. मेमोरी को फ्री करें: सिस्टम को आवश्यक नहीं होने पर आवंटित मेमोरी लौटाएं।

कई आधुनिक प्रोग्रामिंग लैंग्वेज, जैसे C #, Java, विजुअल बेसिक .Net और इस तरह, आवंटित मेमोरी को खाली करने के लिए तथाकथित कचरा कलेक्टर का उपयोग करते हैं। C ++ भाषा की अवधारणा इस तरह के दृष्टिकोण के लिए प्रदान नहीं करती है, हालांकि, हम तथाकथित स्मार्ट पॉइंटर्स का उपयोग करके इसका अनुकरण कर सकते हैं।

आज, C ++ के शस्त्रागार में स्मार्ट पॉइंटर्स हैं, जिन्हें "आउट ऑफ द बॉक्स" कहा जाता है (और C ++ 17 मानक पहले से ही कुछ अप्रचलित प्रकार के स्मार्ट पॉइंटर्स की भाषा से छुटकारा दिलाते हैं), लेकिन यह हमेशा ऐसा नहीं था। आधिकारिक OSG संस्करणों की संख्या 0.9 थी जिसका जन्म 2002 में हुआ था, और पहली आधिकारिक रिलीज़ से पहले तीन और साल थे। उस समय, C ++ मानक ने अभी तक स्मार्ट पॉइंटर्स के लिए प्रदान नहीं किया था, और यहां तक ​​कि अगर आप एक ऐतिहासिक विषयांतर को मानते हैं, तो भाषा स्वयं कठिन समय से गुजर रही थी। तो अपने स्मार्ट पॉइंटर्स के रूप में एक साइकिल की उपस्थिति, जिसे ओएसजी में लागू किया गया है, बिल्कुल आश्चर्य की बात नहीं है। यह तंत्र इंजन की संरचना में गहराई से एकीकृत है, इसलिए इसके संचालन को समझना शुरू से ही आवश्यक है।

3. osg :: ref_ptr <> और osg :: संदर्भित वर्ग


OSG अपने स्मार्ट पॉइंटर तंत्र को प्रदान करता है जो कि ओश :: ref_ptr <> टेम्पलेट क्लास के आधार पर स्वचालित कचरा संग्रहण को लागू करता है। इसके उचित संचालन के लिए, OSG मेमोरी के ब्लॉक के प्रबंधन के लिए एक और ऑसग :: संदर्भित क्लास प्रदान करता है जिसके लिए उनके संदर्भ को गिना जाता है।

Osg :: ref_ptr <> वर्ग कई ऑपरेटरों और तरीकों को प्रदान करता है।

  • get () एक सार्वजनिक विधि है जो एक कच्चे पॉइंटर को लौटाता है, उदाहरण के लिए, जब ऑर्ग :: नोड टेम्पलेट का उपयोग तर्क के रूप में किया जाता है, तो यह विधि ऑस्ग :: नोड * लौटाएगी।
  • ऑपरेटर * () वास्तव में डीरेफेरेंस ऑपरेटर है।
  • ऑपरेटर -> () और ऑपरेटर = () - आपको इस सूचक द्वारा वर्णित वस्तुओं के तरीकों और गुणों तक पहुँचने के लिए एक क्लासिक सूचक के रूप में osg :: ref_ptr <> का उपयोग करने की अनुमति देता है।
  • ऑपरेटर == (), ऑपरेटर! = () और ऑपरेटर! () - आपको स्मार्ट पॉइंटर्स पर तुलना संचालन करने की अनुमति देता है।
  • वैध () एक सार्वजनिक विधि है जो अगर रिटर्न सही है तो प्रबंधित पॉइंटर का सही मान (NULL नहीं) है। अभिव्यक्ति some_ptr.valid () अभिव्यक्ति some_ptr! = NULL के समतुल्य है यदि some_ptr एक स्मार्ट पॉइंटर है।
  • रिलीज़ () एक सार्वजनिक तरीका है, उपयोगी है जब आप किसी फ़ंक्शन से प्रबंधित पता वापस करना चाहते हैं। इसके बारे में बाद में और विस्तार से वर्णन किया जाएगा।

Osg :: संदर्भित वर्ग दृश्य ग्राफ के सभी तत्वों, जैसे नोड्स, ज्यामिति, रेंडरिंग स्टेट्स और मंच पर रखी गई अन्य वस्तुओं के लिए बेस क्लास है। इस प्रकार, दृश्य के मूल नोड को बनाते हुए, हम अप्रत्यक्ष रूप से ऑसग :: संदर्भित श्रेणी द्वारा प्रदान की गई सभी कार्यक्षमता को विरासत में लेते हैं। इसलिए, हमारे कार्यक्रम में एक घोषणा है

osg::ref_ptr<osg::Node> root; 

Osg :: संदर्भित वर्ग में आवंटित मेमोरी ब्लॉक के संदर्भ के लिए एक पूर्णांक काउंटर होता है। यह काउंटर क्लास कंस्ट्रक्टर में शून्य से शुरू होता है। यह एक तरह से बढ़ जाता है जब osg :: ref_ptr <> ऑब्जेक्ट बनाया जाता है। जैसे ही इस पॉइंटर द्वारा बताई गई वस्तु का कोई संदर्भ हटा दिया जाता है, यह काउंटर कम हो जाता है। जब कोई स्मार्ट पॉइंटर्स इसे संदर्भित करने के लिए बंद हो जाता है तो एक वस्तु स्वचालित रूप से नष्ट हो जाती है।

Osg :: संदर्भित श्रेणी में तीन सार्वजनिक विधियाँ हैं:

  • Ref () एक सार्वजनिक विधि है जो 1 संदर्भ गणना द्वारा बढ़ती है।
  • unref () एक सार्वजनिक विधि है, जो 1 संदर्भ गणना से घटती है।
  • ReferenceCount () एक सार्वजनिक विधि है जो संदर्भ काउंटर के वर्तमान मूल्य को लौटाता है, जो कोड डीबग करते समय उपयोगी होता है।

ये तरीके ऑसग :: संदर्भित से प्राप्त सभी वर्गों में उपलब्ध हैं। हालांकि, यह याद रखना चाहिए कि लिंक काउंटर के मैनुअल नियंत्रण से अप्रत्याशित परिणाम हो सकते हैं, और इसका उपयोग करके आपको स्पष्ट रूप से समझना चाहिए कि आप क्या कर रहे हैं।

4. OSG कचरा कैसे एकत्रित करता है और इसकी आवश्यकता क्यों है


स्मार्ट पॉइंटर्स और कचरा संग्रह का उपयोग करने के कई कारण हैं:

  • महत्वपूर्ण त्रुटियों का न्यूनतमकरण: स्मार्ट पॉइंटर्स का उपयोग आपको आवंटन और मेमोरी को मुक्त करने की अनुमति देता है। कोई खतरनाक कच्चे पॉइंटर्स नहीं हैं।
  • प्रभावी स्मृति प्रबंधन: ऑब्जेक्ट के लिए आवंटित मेमोरी को तुरंत मुक्त कर दिया जाता है, जैसे ही ऑब्जेक्ट अनावश्यक हो जाता है, जिससे सिस्टम संसाधनों का किफायती उपयोग होता है।
  • एप्लिकेशन डीबगिंग की सुविधा: किसी ऑब्जेक्ट के लिंक की संख्या को स्पष्ट रूप से ट्रैक करने की क्षमता होने पर, हमारे पास विभिन्न प्रकार के अनुकूलन और प्रयोगों के अवसर हैं।

मान लीजिए कि एक दृश्य ग्राफ में रूट नोड और बच्चे के नोड के कई स्तर शामिल हैं। यदि रूट नोड और सभी चाइल्ड नोड्स का उपयोग osg :: ref_ptr <> क्लास में किया जाता है, तो एप्लिकेशन केवल पॉइंटर को रूट नोड पर ट्रैक कर सकता है। इस नोड को हटाने से सभी बच्चे नोड्स के अनुक्रमिक, स्वचालित हटाने होंगे।



स्मार्ट पॉइंटर्स का उपयोग स्थानीय चर, वैश्विक चर, वर्ग के सदस्यों के रूप में किया जा सकता है, और स्मार्ट पॉइंटर के दायरे से बाहर जाने पर स्वचालित रूप से संदर्भ संख्या में कमी आती है।

ओएसजी डेवलपर्स द्वारा परियोजनाओं में उपयोग के लिए स्मार्ट पॉइंटर्स की दृढ़ता से सिफारिश की जाती है, लेकिन कुछ मूलभूत बिंदु हैं जिन पर आपको ध्यान देना चाहिए:

  • ऑसग के उदाहरण :: संदर्भित और इसके डेरिवेटिव को विशेष रूप से ढेर पर बनाया जा सकता है। इन्हें स्थानीय चर के रूप में स्टैक पर नहीं बनाया जा सकता है, क्योंकि इन वर्गों के विध्वंसक को प्रोटेक घोषित किया जाता है। उदाहरण के लिए

 osg::ref_ptr<osg::Node> node = new osg::Node; //  osg::Node node; //  

  • आप नियमित सी ++ पॉइंटर्स का उपयोग करके अस्थायी दृश्य नोड्स बना सकते हैं, हालांकि, यह दृष्टिकोण असुरक्षित होगा। यह सुनिश्चित करने के लिए स्मार्ट पॉइंटर्स का उपयोग करना बेहतर है कि दृश्य ग्राफ को सही तरीके से प्रबंधित किया जाए।

 osg::Node *tmpNode = new osg::Node; //  ,  ... osg::ref_ptr<osg::Node> node = tmpNode; //         ! 

  • किसी भी मामले में आपको पेड़ में चक्रीय लिंक दृश्यों का उपयोग नहीं करना चाहिए जब नोड सीधे या परोक्ष रूप से कई स्तरों के माध्यम से संदर्भित करता है



दृश्य ग्राफ के उदाहरण ग्राफ में, चाइल्ड 1.1 नोड स्वयं को संदर्भित करता है, और चाइल्ड 2.2 नोड भी बाल 1.2 नोड को संदर्भित करता है। इस तरह के लिंक से लिंक की संख्या और कार्यक्रम के अनिश्चित व्यवहार की गलत गणना हो सकती है।

5. प्रबंधित वस्तुओं पर नज़र रखना


ओएसजी में स्मार्ट पॉइंटर तंत्र के संचालन को स्पष्ट करने के लिए, हम निम्नलिखित सिंथेटिक उदाहरण लिखते हैं

main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/ref_ptr> #include <osg/Referenced> #include <iostream> #endif // MAIN_H 

main.cpp

 #include "main.h" class MonitoringTarget : public osg::Referenced { public: MonitoringTarget(int id) : _id(id) { std::cout << "Constructing target " << _id << std::endl; } protected: virtual ~MonitoringTarget() { std::cout << "Dsetroying target " << _id << std::endl; } int _id; }; int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0); std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl; osg::ref_ptr<MonitoringTarget> anotherTarget = target; std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl; return 0; } 

हम एक ऑग बनाते हैं :: संदर्भित वंशज वर्ग जो कि कंस्ट्रक्टर और डिस्ट्रॉक्टर को छोड़कर कुछ भी नहीं करता है जो यह रिपोर्ट करता है कि इसका उदाहरण बनाया गया है और पहचानकर्ता को प्रदर्शित करता है जो कि निर्धारित होने पर निर्धारित होता है। स्मार्ट पॉइंटर तंत्र का उपयोग करके कक्षा का एक उदाहरण बनाएं

 osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0); 

अगला, हम लक्ष्य वस्तु के लिए संदर्भ काउंटर प्रदर्शित करते हैं

 std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl; 

उसके बाद, एक नया स्मार्ट पॉइंटर बनाएं, जो पिछले पॉइंटर का मान बताता है

 osg::ref_ptr<MonitoringTarget> anotherTarget = target; 

और फिर से संदर्भ काउंटर प्रदर्शित करें

 std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl; 

आइए देखें कि कार्यक्रम के आउटपुट का विश्लेषण करके हमें क्या मिला

 15:42:39:   Constructing target 0 Referenced count before referring: 1 Referenced count after referring: 2 Dsetroying target 0 15:42:42:   

जब क्लास कंस्ट्रक्टर शुरू होता है, तो एक संबंधित संदेश प्रदर्शित होता है, जो हमें बताता है कि ऑब्जेक्ट के लिए मेमोरी आवंटित की गई है और कंस्ट्रक्टर ने ठीक काम किया है। इसके अलावा, स्मार्ट पॉइंटर बनाने के बाद, हम देखते हैं कि निर्मित ऑब्जेक्ट के लिए संदर्भ काउंटर एक से बढ़ गया है। एक नया पॉइंटर बनाना, इसे पुराने पॉइंटर का मान निर्दिष्ट करना अनिवार्य रूप से एक ही ऑब्जेक्ट के लिए एक नया लिंक बनाना है, इसलिए संदर्भ काउंटर को दूसरे द्वारा बढ़ाया जाता है। जब प्रोग्राम से बाहर निकलता है, तो मॉनिटरिंगटार्ग वर्ग का विनाशकर्ता कहा जाता है।



आइए ऐसे कोड को मुख्य () फ़ंक्शन के अंत में जोड़कर एक और प्रयोग करें

 for (int i = 1; i < 5; i++) { osg::ref_ptr<MonitoringTarget> subTarget = new MonitoringTarget(i); } 

इस तरह के "निकास" कार्यक्रम के लिए अग्रणी

 16:04:30:   Constructing target 0 Referenced count before referring: 1 Referenced count after referring: 2 Constructing target 1 Dsetroying target 1 Constructing target 2 Dsetroying target 2 Constructing target 3 Dsetroying target 3 Constructing target 4 Dsetroying target 4 Dsetroying target 0 16:04:32:   

हम स्मार्ट पॉइंटर का उपयोग करके लूप के शरीर में कई ऑब्जेक्ट बनाते हैं। चूंकि सूचक का दायरा केवल इस मामले में लूप के शरीर तक फैला है, जब यह बाहर निकलता है, तो विध्वंसक स्वचालित रूप से कहा जाता है। ऐसा नहीं होगा, काफी स्पष्ट रूप से, हम सामान्य बिंदुओं का उपयोग करेंगे।

मेमोरी के स्वत: मुक्त होने के साथ स्मार्ट पॉइंटर्स के साथ काम करने की एक और महत्वपूर्ण विशेषता है। चूंकि osg :: संदर्भित व्युत्पन्न वर्ग विध्वंसक संरक्षित है, हम ऑब्जेक्ट को हटाने के लिए स्पष्ट रूप से हटाए गए ऑपरेटर को कॉल नहीं कर सकते। किसी ऑब्जेक्ट को हटाने का एकमात्र तरीका लिंक की संख्या को रीसेट करना है। लेकिन फिर मल्टी-थ्रेडेड डेटा प्रोसेसिंग के दौरान हमारा कोड असुरक्षित हो जाता है - हम पहले से हटाए गए ऑब्जेक्ट को किसी अन्य थ्रेड से एक्सेस कर सकते हैं।

सौभाग्य से, OSG अपने ऑब्जेक्ट रिमूवल शेड्यूलर की मदद से इस समस्या का समाधान प्रदान करता है। यह अनुसूचक osg :: DeleteHandler वर्ग के उपयोग पर आधारित है। यह इस तरह से काम करता है कि यह किसी वस्तु को तुरंत हटाने का कार्य नहीं करता है, बल्कि कुछ समय बाद करता है। हटाए जाने वाली सभी वस्तुओं को अस्थायी रूप से संग्रहीत किया जाता है जब तक कि सुरक्षित विलोपन के लिए पल नहीं आता है, और फिर वे सभी एक ही बार में हटा दिए जाते हैं। Osg :: DeleteHandler निष्कासन अनुसूचक को OSG रेंडर बैकेंड द्वारा नियंत्रित किया जाता है।

6. समारोह से लौटें


निम्नलिखित फ़ंक्शन को हमारे उदाहरण कोड में जोड़ें

 MonitoringTarget *createMonitoringTarget(int id) { osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(id); return target.release(); } 

और इस फ़ंक्शन के लिए कॉल के साथ कॉल को लूप में नए ऑपरेटर में बदलें

 for (int i = 1; i < 5; i++) { osg::ref_ptr<MonitoringTarget> subTarget = createMonitoringTarget(i); } 

कॉलिंग रिलीज़ () किसी ऑब्जेक्ट के संदर्भों की संख्या को शून्य तक कम कर देगा, लेकिन मेमोरी को हटाने के बजाय, यह आवंटित मेमोरी को सीधे वास्तविक पॉइंटर लौटाता है। यदि यह पॉइंटर किसी अन्य स्मार्ट पॉइंटर को सौंपा गया है, तो मेमोरी लीक नहीं होगी।

निष्कर्ष


ऑपरेशन के सिद्धांत को समझने के लिए दृश्य ग्राफ और स्मार्ट पॉइंटर्स की अवधारणाएं बुनियादी हैं, और इसलिए ओपनसेकेग्राफ का प्रभावी उपयोग। जैसा कि ओएसजी स्मार्ट पॉइंटर्स का संबंध है, याद रखें कि जब उनका उपयोग बिल्कुल आवश्यक हो

  • सुविधा के दीर्घकालिक भंडारण की उम्मीद है।
  • एक ऑब्जेक्ट किसी अन्य ऑब्जेक्ट के लिए लिंक संग्रहीत करता है
  • आपको किसी फ़ंक्शन से सूचक को वापस करना होगा

लेख में दिया गया नमूना कोड यहां उपलब्ध है

जारी रखने के लिए ...

Source: https://habr.com/ru/post/hi429914/


All Articles