Solved

DirectX Question about vertices, buffers

Posted on 2012-04-11
17
604 Views
Last Modified: 2013-11-08
Experts,

I`m a DirectX newbie. I`m using DirectX 9 on C# .net VS2008 3.5.

Let`s say I have a Vertex List, with indices 0-99. I use a Vertex Buffer to store these. Now my application requires that after an event the new vertices are added to the same list. Lets say 100-199. So on.

What is the most efficient way to to display the vertices in stages?

For example Step 1. Display 0-99
                     Step 2. Calculate vertices 100-199 and then display 0-199
                     Step 3. Calculate vertices 200-299 and then display 0-299
                     So on.

Do I calculate all the vertices before hand and then display them in stages, or can I calculate the vertices in stages and then display them.

Also, the user should be able to use transformations (rotation, shifting) at any stage.

I`m looking for some guidance with this.
0
Comment
Question by:San24
  • 8
  • 7
17 Comments
 
LVL 12

Expert Comment

by:satsumo
ID: 37835611
There isn't one answer to this, it depends what you are doing.

It is better to draw as much as possible in one call.  However, 99 vertices or 199 vertices is plenty to draw in one go.  If I understand the question properly, it's not efficient to draw 0-99, the 0-199 then 0-299, because you're drawing the same vertices many times.  It better to draw each vertex once if you can.  Unless the list of vertices is growing for some reason?

When you say the user should be able to transform, in what sense do you mean?  Moving a camera around?  Moving a model?  Moving a subset of the vertices?

Any set of vertices you render uses the current render state (world transform, texture, shader etc.).  Any need to change to the render state, means you have to render a separate group of vertices.  This is why it is more efficient to group vertices according to render state (texture or shader).  So if vertices 0-99 use texture A and 100-199 use texture B (or transform B), you have to render them as separate batches.

That said, assuming you're using a dynamic vertex buffer you can calculate vertices 100-199 while rendering vertices 0-99 and this can make things faster in many cases.  The hardware can process vertices 0-99 in the same time it takes the CPU to calculate 100-199, so you exploit parallelism.

However, if you only drawing 300 vertices per frame it won't make much difference.  This is only a speed up if the vertices change each frame and there are more of them than you can fit in a vertex buffer.  If they are static then its much faster to just fill a vertex buffer with vertices and render that.

I realise this might be a bit confusing.  If something is not clear I'd be happy to explain more detail if you let me know what applies to this case.
0
 

Author Comment

by:San24
ID: 37837522
Satsumo - Thanks much for your reply.

I agree, it`s not efficient to draw 0-99, then 0-199, then 0-299. etc. The reason I was wondering is this - the user can apply transform at any point. Transform here is moving the model. My thinking was (still a newbie with Graphics). Lets say the user wants to rotate the model at 0-199 stage, doesn`t that mean I need to have the vertices 0-199 drawn?

I should have been clear in my initial post - the vertices 0-99 are the same as 100-199 or 200-299 .. each set is offset by an angle. Think of it as a pattern. So, I`ll be saving time on not calculating them again at each step except for the time to calculate new vertices with the offset.

Let me try explaining with a simple group of numbers (each number is a vertex)

Lets say I have a group of numbers : 5 - 10 - 15 Stage 1

Next I apply an offset of 2 to the group : 7 - 12 - 17 Stage 2

Now at this point I have [5 - 10 - 15] - [7 - 12 - 17] which I should draw, and allow the user to move the model.

Next I get  [5 - 10 - 15] - [7 - 12 - 17]  - [9 - 14 - 19] .etc ..

So, my question is do i add to the vertex buffer at every stage and redraw? What would be the most efficient way to do this. The number of vertices could be quite staggering, so efficiency is important too.

I hope that helps. Any help on this is immensely appreciated.
0
 

Author Comment

by:San24
ID: 37838693
Okay, did a bit more of research and I think I`m headed in the correct direction. I still need help though.

So, What I`m looking to do is draw batches of geometry using Dynamic Vertex Buffers.

Here is the flow (not working)

        /// <summary>
        /// Calculate and Initialize Vertex Data 
        /// </summary>
        private void InitVertex()
        {
            Cnt = FX0.Count;
            VertexData = new List<CustomVertex.PositionNormalTextured>();

            for (i = 0; i < Cnt - 1; i++)
            {
                //Calculate and fill into Veretex Data
            }

            Type VBType = typeof(CustomVertex.PositionNormalTextured);
            VertexFormats VBFormats = CustomVertex.PositionNormalTextured.Format;
            Usage VBUsage = Usage.WriteOnly | Usage.Dynamic;                               //Dynamic Vertex Buffer
            Pool Pl = Pool.Default;                                            

            VBuffer = new VertexBuffer(VBType,
                            VertexData.Length,
                            GDevice,
                            VBUsage,
                            VBFormats,
                            Pl);

           VBuffer.SetData(VertexData.ToArray(), 0, LockFlags.None);
        }

Open in new window


Now, I have an Add Event which should append the Vertex Data

//Here I`m just duplicating the entries. The appended entries will be offset in the final code
  private void Add_Click(object sender, EventArgs e)
        {

//How do I Lock, Copy and Unlock. I`m lost here.


            VertexData.AddRange(VertexData);
            Render();
        }

Open in new window


  /// <summary>
        /// Render
        /// </summary>
        private void Render()
        {
            GDevice.Clear(ClearFlags.Target, System.Drawing.Color.FromArgb(0, 0, 0).ToArgb(), 1.0f, 0);
            
            GDevice.BeginScene();

            InitDevice();
            GDevice.VertexFormat = CustomVertex.PositionNormalTextured.Format;
            GDevice.SetStreamSource(0, VFiberBuffer, 0);
            GDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (VertexData.Count / 3));

            GDevice.EndScene();
            GDevice.Present();
        }

Open in new window



Am I headed in the correct direction? Any sample code will be hugely helpful.
0
 
LVL 12

Expert Comment

by:satsumo
ID: 37838787
I think I understand what you're trying to do but not 100% sure.  I will have a go at answering what I think you're doing, which is appears to be instancing.  Drawing the same thing at different positions and angles.

Assuming vertices 0-99 describe a model, and vertices 100-199 would describe the same model, but rotated 5 degrees.  The fastest way would normally be to draw vertices 0-99, set the world transform to rotate 5 degrees and draw vertices 0-99 again.  You don't need vertices 100-199 at all.

Assuming the vertices of the model don't change, the vertex buffer containing the vertices can stay in GPU ram all the time.  You can render it as many times as you want without sending data from CPU to GPU (a fairly slow process).  Even if the vertex buffer is not stored in GPU ram, you don't have to recalculate vertices 100-199 so that saves CPU time.

If the model is small, say 16 vertices, the time taken to repeatedly change the transform might exceed the time it takes to calculate new vertices and send the extra data.  In that case it might be faster to calculate vertices into a big buffer and render it all in one go.  But note, that is quite a few mights, you can only really find out by testing it.  As I say, there isn't single answer.

A good way to think about it is anything that changes is slow, vertex, matrix, texture or shader.  Speed means changing the smallest amount of things possible.  Changing the matrix is less change than making a new set of vertices (unless the set of vertices is very small).
0
 
LVL 12

Expert Comment

by:satsumo
ID: 37838854
Oops, you added those while I was typing that last answer.  The best answer depends on what you are going to draw?  Dynamic vertex buffer are useful for models that change shape each frame.  Things like explosions, or fluid similations.  If the model is something like a spaceship, it doesn't change shape, it just moves around.

Dynamic vertex buffers are also useful when you are trying to save GPU memory (like on a phone).  Instead of storing all the models in static buffers you copy them into the dynamic buffer and render it for each set of data.

I'd suggest a method if I knew exactly what kind of thing you were rendering, I guess theres a reason you've chosen dynamic buffers?
0
 
LVL 12

Expert Comment

by:satsumo
ID: 37838894
Now I see what's happening in that code.  That code is filling the vertex buffer with new vertices while the GPU is rendering the previous vertices.  It's an effective way to render lots of geometry from CPU ram when you have more than will fit in GPU ram.  It's not as fast as static vertex buffers, but its a useful technique, I use it when rendering objects with few vertices.

Still, the same answer applies, if all that changes is the transform, you can render the same vertices again and it will save time.  Particularly in the case of a dynamic buffer.
0
 
LVL 12

Expert Comment

by:satsumo
ID: 37838958
Using the method the process would normally go like this:

Fill buffer with vertices for model A (vertices 0-50)
For each instance of model A set the transform and render vertices 0-50
Fill buffer with vertices of model B (50-120)
For each instance of model B set the transform and render vertices 50-120
Continue this way until all vertices in the buffer are used.
Start again from vertex 0.

If the models in question are small there's no gain from doing the instancing part.  This would be the case with something like a particle system, where each item is a quad.
0
How to improve team productivity

Quip adds documents, spreadsheets, and tasklists to your Slack experience
- Elevate ideas to Quip docs
- Share Quip docs in Slack
- Get notified of changes to your docs
- Available on iOS/Android/Desktop/Web
- Online/Offline

 

Author Comment

by:San24
ID: 37839107
Bingo! You described exactly what I`m trying to achieve -

Assuming vertices 0-99 describe a model, and vertices 100-199 would describe the same model, but rotated 5 degrees.  The fastest way would normally be to draw vertices 0-99, set the world transform to rotate 5 degrees and draw vertices 0-99 again.  You don't need vertices 100-199 at all.

So, lets start with a solution for that.

What I initially did was : Step 1: Display 0-99
                                       Step 2: Set Clear Device to false
                                       Step 3: Rotate by angle
                                       Step 4: Display 0-99
                                       Repeat.

What this is doing is pretty much drawing vertices on top of each other.

This gives me the intended results. But if the user wants to transform the model, lets say he wants to see the side view, all the data is lost except the last set of vertices.
So, my thinking was to have all the vertices drawn.
0
 
LVL 12

Expert Comment

by:satsumo
ID: 37842523
I'm not sure what step 2 is for? Are you redrawing a whole scene but without clearing the display?  Based on the code you shown I would do something like this
GDevice.Clear(ClearFlags.Target, System.Drawing.Color.FromArgb(0, 0, 0).ToArgb(), 1.0f, 0);
            
GDevice.BeginScene();

GDevice.VertexFormat = CustomVertex.PositionNormalTextured.Format;
GDevice.SetStreamSource(0, VFiberBuffer, 0);
GDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (VertexData.Count / 3));

GDevice.SetTransform(D3DTS_WORLD, &mtxObject);
GDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (VertexData.Count / 3));

GDevice.EndScene();
GDevice.Present();

Open in new window

mtxObject is a D3DMATRIX that transforms the model in some way; rotate, translate or scale.  You can repeat the set matrix/draw model process as often as you like.  The matrix can be obtained by any means, for example you could put that code in a loop and recalculate a rotation matrix each time around the loop.

What normally happens in a game or an editor is that there is an object which represent the location of a model in the world, each object has it own matrix.  So if the model is a box of bullets, there can be ten 'bullets' objects, each managing a matrix but all using the same vertices to render.  Typically that means there is also a 'model' object, which manages the set of vertices.  Is this making sense to you?
0
 
LVL 12

Expert Comment

by:satsumo
ID: 37842557
To be really fast, a game will actually build a list of renderable objects.  It adds each 'bullets' object that can be seen to a list managed by the model.  When it comes to rendering, the model sets the vertices then goes through it's list of visible objects, setting the matrix and rendering.  That's fairly involved for someone who is just starting though.
0
 

Author Comment

by:San24
ID: 37844450
Satsumo..I`ve been working on a approach where I update the vertex buffer. I`ll post the code shortly -

So, the idea is to append the vertex buffer which contains the vertices which have been rotated. The below code doesn`t seem to work though, the vertex buffer is not being appended.

Shouldn`t LockFlags.NoOverwrite append new vertices to the vertices already contained in the buffer? What am I missing?

 VertexBuffer.SetData(NewVertexData.ToArray(), 0, LockFlags.NoOverwrite);

Open in new window

0
 
LVL 12

Expert Comment

by:satsumo
ID: 37845029
NoOverwrite is quite literal.  It means you won't change anything in the vertex buffer while its locked.  I've never used it, can't see the point to be honest.  For what you're doing I would use LockFlags.Discard, which tells the driver that your going to change the vertices in the locked area.  That way it can choose to give you a different buffer if the existing buffer is still being used for rendering.  Yes, the naming of the flags is a bit confusing.

However, that might not be the problem.  Does the code use an index of zero all the time?  Shouldn't the index change according to which vertices you intend to use?
0
 

Author Comment

by:San24
ID: 37853212
I was off most of the day today, I`ll update tomorrow. Not much progress since I last posted. Do I need IndexBuffers? Will it make it simpler? My code initially wasn`t using it, but I can incorporate it.
0
 

Accepted Solution

by:
San24 earned 0 total points
ID: 37857889
Here is the code. Hopefully this will give a better picture of what I`m trying to do. I have requested for attention through the comments in the code. I`m still very new to Graphics and DirectX, patience is much appreciated.

Let me know if this is a good approach and I`m in the right direction

        /// <summary>
        /// Initialize
        /// Set the VetrexBuffer and the IndexBuffer
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Initialize_Click(object sender, EventArgs e)
        {
            Type VBType = typeof(CustomVertex.PositionNormalTextured);
            VertexFormats VBFormats = CustomVertex.PositionNormalTextured.Format;
            Usage VBUsage = Usage.WriteOnly | Usage.SoftwareProcessing;
            Pool Pl = Pool.Managed;
            //Usage VBUsage = Usage.WriteOnly | Usage.Dynamic;
            //Pool Pl = Pool.Default;

            CVLength = FX0.Count * 3 * 2 * 2;                  //FX$, FY$ and FZ$ contain the X, Y, Z Values. CVLength will be the number of vertices for a single pass.

            VXtrBuffer = new VertexBuffer(VBType,
                            CVLength * 50,                     //Have enough for 50 passes, same with index buffer
                            GDevice,
                            VBUsage,
                            VBFormats,
                            Pl);

            IXtrBuffer = new IndexBuffer(GDevice, sizeof(ushort) * CVLength * 50, VBUsage, Pl, true);

            InitXtr();             //Xtr
        }

Open in new window


Here is heart of the code, I need to generate the same pattern offset by an angle for every pass.

        /// <summary>
        /// Add Pass
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Add_Click(object sender, EventArgs e)
        {
            AddPass();
        }

        /// <summary>
        /// Add Pass.. See comments within code
        /// This works for Pass 1, subsequent passes don`t seem to work.
        /// What I`m trying to do, is draw subsequent passes offset by an angle.
        /// </summary>
        private void AddPass()
        {
            int Cnt = 0;
            int i = 0;
            int Ind = 0;
            Vector3 V0, V1, V2, V3;
            Vector3 N1, N2;
            float VertX0 = 0, VertY0 = 0, VertZ0 = 0;
            float VertX1 = 0, VertY1 = 0, VertZ1 = 0;
            float VertX2 = 0, VertY2 = 0, VertZ2 = 0;
            float VertX3 = 0, VertY3 = 0, VertZ3 = 0;

            //FX$,FY$, FZ$ are the X, Y, Z coordinates
            Cnt = FX0.Count;                                

            //An array to store the vertices for a single Pass
            XtrTexture = new CustomVertex.PositionNormalTextured[CVLength];
            
            //For every call of the function, the angle is incremented by 5. 
            //This means the same pattern is offset by Ang everytime the function is called i,e, every Pass
            Ang = Ang + 5;

            //This is the main code which generates the pattern to be dispalyed.
            //This works good. This is not the most efficient way to do this, but for the sake of simplicity, I`ll go with this for now.
            for (i = 0; i < Cnt - 1; i++)
            {
                //Triangle 1 and 2 [Top]
                VertX0 = (float)(FX0[i] * Math.Cos(Ang) - FY0[i] * Math.Sin(Ang));
                VertY0 = (float)(FX0[i] * Math.Sin(Ang) + FY0[i] * Math.Cos(Ang));
                VertZ0 = (float)(FZ0[i]);
                V0 = new Vector3(VertX0, VertY0, VertZ0);

                VertX1 = (float)(FX0[i + 1] * Math.Cos(Ang) - FY0[i + 1] * Math.Sin(Ang));
                VertY1 = (float)(FX0[i + 1] * Math.Sin(Ang) + FY0[i + 1] * Math.Cos(Ang));
                VertZ1 = (float)(FZ0[i + 1]);
                V1 = new Vector3(VertX1, VertY1, VertZ1);

                VertX2 = (float)(FX1[i + 1] * Math.Cos(Ang) - FY1[i + 1] * Math.Sin(Ang));
                VertY2 = (float)(FX1[i + 1] * Math.Sin(Ang) + FY1[i + 1] * Math.Cos(Ang));
                VertZ2 = (float)(FZ1[i + 1]);
                V2 = new Vector3(VertX2, VertY2, VertZ2);

                VertX3 = (float)(FX1[i] * Math.Cos(Ang) - FY1[i] * Math.Sin(Ang));
                VertY3 = (float)(FX1[i] * Math.Sin(Ang) + FY1[i] * Math.Cos(Ang));
                VertZ3 = (float)(FZ1[i]);
                V3 = new Vector3(VertX3, VertY3, VertZ3);

                N1 = CalcNormal(V0, V1, V2);
                N2 = CalcNormal(V2, V3, V0);

                XtrTexture[Ind + 0] = new CustomVertex.PositionNormalTextured(V0, N1, 0f, .5f);
                XtrTexture[Ind + 1] = new CustomVertex.PositionNormalTextured(V1, N1, 1.0f, 0.5f);
                XtrTexture[Ind + 2] = new CustomVertex.PositionNormalTextured(V2, N1, 1.0f, 1.0f);

                XtrTexture[Ind + 3] = new CustomVertex.PositionNormalTextured(V2, N2, 1.0f, 1.0f);
                XtrTexture[Ind + 4] = new CustomVertex.PositionNormalTextured(V3, N2, 0.0f, 1.0f);
                XtrTexture[Ind + 5] = new CustomVertex.PositionNormalTextured(V0, N2, 0f, 0.5f);

                //Triangle 3 and 4 [Bottom]
                VertX0 = (float)(FX2[i] * Math.Cos(Ang) - FY2[i] * Math.Sin(Ang));
                VertY0 = (float)(FX2[i] * Math.Sin(Ang) + FY2[i] * Math.Cos(Ang));
                VertZ0 = (float)(FZ2[i]);
                V0 = new Vector3(VertX0, VertY0, VertZ0);

                VertX1 = (float)(FX2[i + 1] * Math.Cos(Ang) - FY2[i + 1] * Math.Sin(Ang));
                VertY1 = (float)(FX2[i + 1] * Math.Sin(Ang) + FY2[i + 1] * Math.Cos(Ang));
                VertZ1 = (float)(FZ2[i + 1]);
                V1 = new Vector3(VertX1, VertY1, VertZ1);

                VertX2 = (float)(FX0[i + 1] * Math.Cos(Ang) - FY0[i + 1] * Math.Sin(Ang));
                VertY2 = (float)(FX0[i + 1] * Math.Sin(Ang) + FY0[i + 1] * Math.Cos(Ang));
                VertZ2 = (float)(FZ0[i + 1]);
                V2 = new Vector3(VertX2, VertY2, VertZ2);

                VertX3 = (float)(FX0[i] * Math.Cos(Ang) - FY0[i] * Math.Sin(Ang));
                VertY3 = (float)(FX0[i] * Math.Sin(Ang) + FY0[i] * Math.Cos(Ang));
                VertZ3 = (float)(FZ0[i]);
                V3 = new Vector3(VertX3, VertY3, VertZ3);

                N1 = CalcNormal(V0, V1, V2);
                N2 = CalcNormal(V2, V3, V0);

                XtrTexture[Ind + 6] = new CustomVertex.PositionNormalTextured(V0, N1, 0.0f, 0.0f);
                XtrTexture[Ind + 7] = new CustomVertex.PositionNormalTextured(V1, N1, 1.0f, 0f);
                XtrTexture[Ind + 8] = new CustomVertex.PositionNormalTextured(V2, N1, 1.0f, 0.5f);

                XtrTexture[Ind + 9] = new CustomVertex.PositionNormalTextured(V2, N2, 1.0f, 0.5f);
                XtrTexture[Ind + 10] = new CustomVertex.PositionNormalTextured(V3, N2, 0.0f, 0.5f);
                XtrTexture[Ind + 11] = new CustomVertex.PositionNormalTextured(V0, N2, 0.0f, 0.0f);

                Ind += 12;
            }

            //Question
            //I want to update the index buffer, I`m not sure if this is the correct way of doing this. 
            //1st Pass I`m locking and updating from 0 to CVLength
            //2nd Pass I`m locking from the CVLength to 2 * CVLength [since the number of indices are doubled, only update the new vertices?]
            //3rd Pass.....nth Pass
            using (GStream = IXtrBuffer.Lock((Pass * CVLength), ((Pass * CVLength) + CVLength) * sizeof(ushort), LockFlags.None))
            {
                for (int j = (Pass * CVLength); j < (Pass * CVLength) + CVLength; j += 12)
                {
                    ushort[] Indices = {
                                        (ushort)(j + 0),
                                        (ushort)(j + 1 ),
                                        (ushort)(j + 2), 
                                        (ushort)(j + 3), 
                                        (ushort)(j + 4), 
                                        (ushort)(j + 5),
                                        (ushort)(j + 6),
                                        (ushort)(j + 7),
                                        (ushort)(j + 8), 
                                        (ushort)(j + 9), 
                                        (ushort)(j + 10), 
                                        (ushort)(j + 11)
                    };
                    GStream.Write(Indices);
                }

                IXtrBuffer.Unlock();
            }

            //Question
            //Just like the index buffer, updating the new Vertices here
            //Is VXtrBuffer appended with new vertices for each pass (what I`m trying to do) or VXtrbuffer gets reset and get new set of vertices?
            GStream = VXtrBuffer.Lock((Pass * CVLength), ((Pass * CVLength) + CVLength) * 32, LockFlags.None);
            GStream.Write(XtrTexture);
            VXtrBuffer.Unlock();

            GDevice.Clear(ClearFlags.Target, System.Drawing.Color.FromArgb(0, 0, 0).ToArgb(), 1.0f, 0);

            GDevice.BeginScene();
            
            InitDeviceXtr();
            GDevice.VertexFormat = CustomVertex.PositionNormalTextured.Format;
            
            //Question
            //The function Setstream, comments in the parameter 
            GDevice.SetStreamSource(0, 
                                   VXtrBuffer,                                   //If the Vertex Buffer is appended, I should offset it for each pass
                                   (Pass * CVLength) * 32);                        //Offset for each pass
            GDevice.Indices = IXtrBuffer;
           
            //Question
            //The function DrawIndexPrimitives, comments in the paramter
            GDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList,             //Primitive Type
                                          Pass * CVLength,                        //Base Vertex - Offset applied?
                                          0,                                      //MinIntVertex - Not sure? Should this point to the index of the next batch of vertices
                                          CVLength,                               //Number of Vertices                                                       
                                          (Pass * CVLength),                      //Start Index         
                                          ((Pass * CVLength) + CVLength) / 3);    //Primitive Count

            //Increment the Pass Number
            Pass++;                                       

            GDevice.EndScene();
            GDevice.Present();
        }

Open in new window

0
 

Author Closing Comment

by:San24
ID: 38140765
Problem resolved.
0

Featured Post

Maximize Your Threat Intelligence Reporting

Reporting is one of the most important and least talked about aspects of a world-class threat intelligence program. Here’s how to do it right.

Join & Write a Comment

Every server (virtual or physical) needs a console: and the console can be provided through hardware directly connected, software for remote connections, local connections, through a KVM, etc. This document explains the different types of consol…
What do we know about Legacy Video Conferencing? - Full IT support needed! - Complicated systems at outrageous prices! - Intense training required! Highfive believes we need to embrace a new alternative.
Access reports are powerful and flexible. Learn how to create a query and then a grouped report using the wizard. Modify the report design after the wizard is done to make it look better. There will be another video to explain how to put the final p…
This video shows how to remove a single email address from the Outlook 2010 Auto Suggestion memory. NOTE: For Outlook 2016 and 2013 perform the exact same steps. Open a new email: Click the New email button in Outlook. Start typing the address: …

746 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

9 Experts available now in Live!

Get 1:1 Help Now